@enricai/barnacle 1.6.12 → 1.6.14
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/README.md +113 -9
- package/dist/api/schemas/submissions.d.ts +14 -0
- package/dist/api/schemas/submissions.d.ts.map +1 -1
- package/dist/api/schemas/submissions.js +1 -0
- package/dist/api/schemas/submissions.js.map +1 -1
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/bedrock.d.ts +8 -5
- package/dist/lib/bedrock.d.ts.map +1 -1
- package/dist/lib/bedrock.js +8 -5
- package/dist/lib/bedrock.js.map +1 -1
- package/dist/lib/http.d.ts +4 -3
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +25 -3
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/telemetry/beacon-capture.d.ts +45 -2
- package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/beacon-capture.js +36 -0
- package/dist/lib/telemetry/beacon-capture.js.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.d.ts +41 -1
- package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.js +29 -2
- package/dist/lib/telemetry/reconciliation-record.js.map +1 -1
- package/dist/lib/telemetry/reconciliation-source.d.ts +7 -1
- package/dist/lib/telemetry/reconciliation-source.d.ts.map +1 -1
- package/dist/lib/telemetry/reconciliation-source.js +21 -5
- package/dist/lib/telemetry/reconciliation-source.js.map +1 -1
- package/dist/lib/telemetry/run-telemetry.d.ts +67 -0
- package/dist/lib/telemetry/run-telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry/run-telemetry.js +55 -0
- package/dist/lib/telemetry/run-telemetry.js.map +1 -0
- package/dist/lib/telemetry/submission-capture.d.ts +11 -4
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-capture.js +1 -0
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/lib/telemetry/submission-reader.d.ts +11 -3
- package/dist/lib/telemetry/submission-reader.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-reader.js +32 -8
- package/dist/lib/telemetry/submission-reader.js.map +1 -1
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +19 -0
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/plugins/config-plugin.js +3 -3
- package/dist/plugins/loader.d.ts +20 -0
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +94 -17
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/deep-locator-actuate.d.ts +94 -0
- package/dist/scraper/deep-locator-actuate.d.ts.map +1 -0
- package/dist/scraper/deep-locator-actuate.js +216 -0
- package/dist/scraper/deep-locator-actuate.js.map +1 -0
- package/dist/scraper/deep-locator-candidates.d.ts +105 -18
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +238 -46
- package/dist/scraper/deep-locator-candidates.js.map +1 -1
- package/dist/scraper/deep-locator-click.d.ts +71 -0
- package/dist/scraper/deep-locator-click.d.ts.map +1 -0
- package/dist/scraper/deep-locator-click.js +71 -0
- package/dist/scraper/deep-locator-click.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +344 -14
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -1
- package/dist/scraper/deep-locator-fake.js +509 -7
- package/dist/scraper/deep-locator-fake.js.map +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +220 -0
- package/dist/scraper/deep-locator-scan.d.ts.map +1 -0
- package/dist/scraper/deep-locator-scan.js +386 -0
- package/dist/scraper/deep-locator-scan.js.map +1 -0
- package/dist/scraper/fake-timer-advance.d.ts +47 -0
- package/dist/scraper/fake-timer-advance.d.ts.map +1 -0
- package/dist/scraper/fake-timer-advance.js +44 -0
- package/dist/scraper/fake-timer-advance.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +80 -9
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +417 -63
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +28 -0
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +30 -0
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +13 -0
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +29 -0
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-ip.d.ts +69 -0
- package/dist/scraper/session-ip.d.ts.map +1 -0
- package/dist/scraper/session-ip.js +88 -0
- package/dist/scraper/session-ip.js.map +1 -0
- package/dist/scraper/session-shared.d.ts +9 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +14 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +33 -2
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +1 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +48 -16
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.js +3 -3
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +20 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/dist/testing/integration-runner.d.ts.map +1 -1
- package/dist/testing/integration-runner.js +6 -7
- package/dist/testing/integration-runner.js.map +1 -1
- package/package.json +3 -2
|
@@ -29,6 +29,7 @@ exports.windowHasTransitionBody = windowHasTransitionBody;
|
|
|
29
29
|
exports.windowHasAdvanceTransition = windowHasAdvanceTransition;
|
|
30
30
|
exports.shouldVetoFallbackAdvance = shouldVetoFallbackAdvance;
|
|
31
31
|
exports.isDomOnlyAdvanceVerified = isDomOnlyAdvanceVerified;
|
|
32
|
+
exports.isClickViewSwapVerified = isClickViewSwapVerified;
|
|
32
33
|
exports.countNgInvalidContainers = countNgInvalidContainers;
|
|
33
34
|
exports.describeAttemptEffectSignals = describeAttemptEffectSignals;
|
|
34
35
|
exports.shouldSkipTechnique = shouldSkipTechnique;
|
|
@@ -56,6 +57,7 @@ exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
|
56
57
|
exports.attachToSurfacedInput = attachToSurfacedInput;
|
|
57
58
|
exports.surfaceAndUpload = surfaceAndUpload;
|
|
58
59
|
exports.parseSelectStep = parseSelectStep;
|
|
60
|
+
exports.parseFillStep = parseFillStep;
|
|
59
61
|
exports.parseRadioStep = parseRadioStep;
|
|
60
62
|
exports.pollEnumerate = pollEnumerate;
|
|
61
63
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
@@ -87,7 +89,10 @@ const schemas_1 = require("../lib/llm/schemas");
|
|
|
87
89
|
const logging_1 = require("../lib/logging");
|
|
88
90
|
const call_capture_1 = require("../lib/telemetry/call-capture");
|
|
89
91
|
const call_types_1 = require("../lib/telemetry/call-types");
|
|
92
|
+
const deep_locator_actuate_1 = require("../scraper/deep-locator-actuate");
|
|
90
93
|
const deep_locator_candidates_1 = require("../scraper/deep-locator-candidates");
|
|
94
|
+
const deep_locator_click_1 = require("../scraper/deep-locator-click");
|
|
95
|
+
const deep_locator_scan_1 = require("../scraper/deep-locator-scan");
|
|
91
96
|
const errors_2 = require("../scraper/errors");
|
|
92
97
|
const frame_target_1 = require("../scraper/frame-target");
|
|
93
98
|
const phantom_click_1 = require("../scraper/phantom-click");
|
|
@@ -476,7 +481,14 @@ exports.TRAILING_GRACE_WINDOW = 2;
|
|
|
476
481
|
* shape via Runtime.callFunctionOn.
|
|
477
482
|
*/
|
|
478
483
|
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "" }; const t = b.innerText || ""; return { html: (b.outerHTML || "").length, text: t.length + ":" + t.slice(0, 200) }; })()`;
|
|
479
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
486
|
+
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
487
|
+
* `url()` rejects (OOPIF detached by the submit it just fired) can still
|
|
488
|
+
* report the main frame's post-navigation URL instead of throwing the
|
|
489
|
+
* whole attempt out of `executeStepWithHealing`.
|
|
490
|
+
*/
|
|
491
|
+
async function snapshotPage(target, signalCounter, page) {
|
|
480
492
|
let bodyHtmlLength = 0;
|
|
481
493
|
let visibleTextSignature = "";
|
|
482
494
|
try {
|
|
@@ -495,9 +507,16 @@ async function snapshotPage(target, signalCounter) {
|
|
|
495
507
|
// Snapshot is observational; on failure, defaults to 0/"" so the verifier
|
|
496
508
|
// sees no delta. Real state-class checks already cover the verified path.
|
|
497
509
|
}
|
|
510
|
+
// A resolved child FrameTarget's url() reads location.href off the CDP
|
|
511
|
+
// frame session (frame-target.ts's childFrameTarget), which rejects (or
|
|
512
|
+
// trips its watchdog) once the OOPIF detaches — most commonly right after
|
|
513
|
+
// a submit click tears down the wizard iframe. Falling back to page.url()
|
|
514
|
+
// lets the post-submit navigation still register as a urlChanged signal
|
|
515
|
+
// instead of throwing the whole step out of the cascade.
|
|
516
|
+
const url = page ? await target.url().catch(() => page.url()) : await target.url();
|
|
498
517
|
return {
|
|
499
518
|
networkCount: signalCounter.n,
|
|
500
|
-
url
|
|
519
|
+
url,
|
|
501
520
|
bodyHtmlLength,
|
|
502
521
|
visibleTextSignature,
|
|
503
522
|
};
|
|
@@ -874,6 +893,43 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
874
893
|
// non-advancing POST) does not verify it.
|
|
875
894
|
return networkIsRealAdvance || urlChanged;
|
|
876
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Client-side view-swap verification gate. Credits a click that produces
|
|
898
|
+
* substantial DOM growth with zero network as a verified step outcome when
|
|
899
|
+
* the step is a plain client-side reveal (not a submit/final step, not an
|
|
900
|
+
* advance/'Next' step on a site with advanceTransitionBodyPattern configured).
|
|
901
|
+
*
|
|
902
|
+
* **Why this exists:** SPA/iframe wizards often swap visible screens
|
|
903
|
+
* (e.g. method-chooser → basic-info form) via client-side DOM manipulation
|
|
904
|
+
* with zero network requests. The existing verifyDomEffect (line 4866-4891)
|
|
905
|
+
* explicitly returns false for non-radio/checkbox clicks, routing those
|
|
906
|
+
* to the network/URL signal — which correctly verifies navigation but
|
|
907
|
+
* false-negatives client-side view swaps. This gate fills that gap.
|
|
908
|
+
*
|
|
909
|
+
* **Threshold rationale:** 5000B is 10× the trivial boundary (500B) and
|
|
910
|
+
* safely distinguishes real view swaps (measured case: +49518B) from
|
|
911
|
+
* validation-error reflows (typically < 2KB per codebase evidence at
|
|
912
|
+
* line 2040-2044: error text capped at 200 chars, container markup adds
|
|
913
|
+
* 150-350B per error, worst-case ~10 errors = ~3500B max).
|
|
914
|
+
*
|
|
915
|
+
* **Scope guards:** Excludes final/submit steps (those require real network
|
|
916
|
+
* per isSubmitRevealedInvalid + LLM submit judge) and advance-pattern steps
|
|
917
|
+
* (those require real transition per isDomOnlyAdvanceVerified/isAdvanceStalled
|
|
918
|
+
* to avoid wizard-ATS autosave-vs-transition ambiguity).
|
|
919
|
+
*/
|
|
920
|
+
function isClickViewSwapVerified(params) {
|
|
921
|
+
const VIEW_SWAP_MIN_BYTES = 5000;
|
|
922
|
+
const { resolvedAction, isFinalStep, submitStep, isAdvanceWithPattern, networkDelta, bytesDelta, } = params;
|
|
923
|
+
if (resolvedAction?.method !== "click")
|
|
924
|
+
return false;
|
|
925
|
+
if (isFinalStep || submitStep)
|
|
926
|
+
return false;
|
|
927
|
+
if (isAdvanceWithPattern)
|
|
928
|
+
return false;
|
|
929
|
+
if (networkDelta !== 0)
|
|
930
|
+
return false;
|
|
931
|
+
return bytesDelta >= VIEW_SWAP_MIN_BYTES;
|
|
932
|
+
}
|
|
877
933
|
/**
|
|
878
934
|
* Read-only count of ng-invalid form controls on the resolved frame. Side-effect-free
|
|
879
935
|
* counterpart to `probeFormValidityBeforeSubmit` (which also auto-fills
|
|
@@ -2277,7 +2333,7 @@ function isUploadAffordanceLabel(label) {
|
|
|
2277
2333
|
return /\b(upload|browse|select file|choose file|attach|add (a )?(resume|cv|file)|resume\/cv)\b/.test(norm);
|
|
2278
2334
|
}
|
|
2279
2335
|
/**
|
|
2280
|
-
* Materialize the in-memory
|
|
2336
|
+
* Materialize the in-memory upload fixture to a temp file so CDP
|
|
2281
2337
|
* `DOM.setFileInputFiles` (which requires a filesystem path, unlike
|
|
2282
2338
|
* Playwright's `locator.setInputFiles`) can reference it. Only used as a
|
|
2283
2339
|
* fallback when the on-disk fixture path is unavailable. Process-scoped — the
|
|
@@ -2687,6 +2743,40 @@ function parseSelectStep(instruction) {
|
|
|
2687
2743
|
const questionLabel = quoted.find((q) => q.trim() !== option)?.trim() ?? null;
|
|
2688
2744
|
return { option, questionLabel };
|
|
2689
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Parse a declarative TEXT-FILL flow step into the field label to target and
|
|
2748
|
+
* the value to type into it.
|
|
2749
|
+
*
|
|
2750
|
+
* Why this exists (sibling of `parseSelectStep`/`parseRadioStep`): the deep-
|
|
2751
|
+
* locator cascade's candidate ranking (`resolveDeepLocatorCandidates`'s
|
|
2752
|
+
* `scoreCandidate`) ranks by the instruction's quoted phrases, which for a
|
|
2753
|
+
* fill step is only the VALUE ("Fill in the First Name field with
|
|
2754
|
+
* 'Reginald'" quotes just `'Reginald'`) — no candidate's accessible name
|
|
2755
|
+
* (e.g. "First Name") ever matches a person's name, so every candidate ties
|
|
2756
|
+
* at score 0 and DOM order decides, clicking whatever happens to be first
|
|
2757
|
+
* rather than filling the named field. This parser recovers the FIELD LABEL
|
|
2758
|
+
* ("First Name") so the caller can match it directly against a candidate's
|
|
2759
|
+
* accessible name instead of ranking by the value.
|
|
2760
|
+
*
|
|
2761
|
+
* Recognizes the flow's conventional phrasing (confirmed against
|
|
2762
|
+
* `src/recon/fixtures/shipped-ats-flow-steps.json`):
|
|
2763
|
+
* "Fill in the First Name field with 'Reginald'",
|
|
2764
|
+
* "Fill the signature field with 'Name'",
|
|
2765
|
+
* "Fill in the Acme Non-Employee ID textbox with 'NA'".
|
|
2766
|
+
* Returns null when the step doesn't match this shape (no "fill" verb, no
|
|
2767
|
+
* field/input/textbox noun, or no quoted value) so the caller falls through
|
|
2768
|
+
* to the normal cascade unchanged.
|
|
2769
|
+
*/
|
|
2770
|
+
function parseFillStep(instruction) {
|
|
2771
|
+
const match = instruction.match(/\bfill(?:\s+in)?\s+(?:the\s+)?(.+?)\s+(?:field|input|textbox)\s+with\s+'([^']+)'/i);
|
|
2772
|
+
if (!match)
|
|
2773
|
+
return null;
|
|
2774
|
+
const fieldLabel = match[1]?.trim();
|
|
2775
|
+
const value = match[2]?.trim();
|
|
2776
|
+
if (!fieldLabel || !value)
|
|
2777
|
+
return null;
|
|
2778
|
+
return { fieldLabel, value };
|
|
2779
|
+
}
|
|
2690
2780
|
/**
|
|
2691
2781
|
* Parse a single-choice RADIO flow step into the option to click and (when
|
|
2692
2782
|
* present) the question label that scopes which radio group it targets.
|
|
@@ -2740,6 +2830,44 @@ function parseRadioStep(instruction) {
|
|
|
2740
2830
|
const questionLabel = quoted.find((q) => q.trim() !== option)?.trim() ?? null;
|
|
2741
2831
|
return { option, questionLabel };
|
|
2742
2832
|
}
|
|
2833
|
+
function resolveDeepLocatorActuation(step) {
|
|
2834
|
+
const selectParsed = parseSelectStep(step);
|
|
2835
|
+
if (selectParsed)
|
|
2836
|
+
return { kind: "select", value: selectParsed.option };
|
|
2837
|
+
const fillParsed = parseFillStep(step);
|
|
2838
|
+
if (fillParsed)
|
|
2839
|
+
return { kind: "fill", value: fillParsed.value };
|
|
2840
|
+
return { kind: "click" };
|
|
2841
|
+
}
|
|
2842
|
+
/** Whitespace-collapsed, lowercased comparison key for {@link findDeepLocatorCandidateByFieldLabel}. */
|
|
2843
|
+
function normalizeFieldLabel(text) {
|
|
2844
|
+
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Finds the candidate whose accessible name identifies the named field —
|
|
2848
|
+
* `fieldLabel` (from {@link parseFillStep}/{@link parseSelectStep}), NOT the
|
|
2849
|
+
* value being written — among a frame's deepLocator candidates. Exact match
|
|
2850
|
+
* wins first (a bare "First Name" input against a `fieldLabel` of "First
|
|
2851
|
+
* Name"); otherwise a substring match either direction (a `fieldLabel` of
|
|
2852
|
+
* "Acme Non-Employee ID" against an accessible name of "Non-Employee ID", or
|
|
2853
|
+
* the reverse) so minor phrasing drift between the flow's field noun and the
|
|
2854
|
+
* control's own label still resolves. Returns `null` — never a guess — when
|
|
2855
|
+
* no candidate's accessible name relates to `fieldLabel` at all, so the
|
|
2856
|
+
* caller can refuse to act rather than fill/click the wrong control.
|
|
2857
|
+
*/
|
|
2858
|
+
function findDeepLocatorCandidateByFieldLabel(candidates, fieldLabel) {
|
|
2859
|
+
const normalizedLabel = normalizeFieldLabel(fieldLabel);
|
|
2860
|
+
if (!normalizedLabel)
|
|
2861
|
+
return null;
|
|
2862
|
+
const named = candidates
|
|
2863
|
+
.map((candidate) => ({ candidate, text: normalizeFieldLabel(candidate.accessibleText) }))
|
|
2864
|
+
.filter((entry) => entry.text.length > 0);
|
|
2865
|
+
const exact = named.find((entry) => entry.text === normalizedLabel);
|
|
2866
|
+
if (exact)
|
|
2867
|
+
return exact.candidate;
|
|
2868
|
+
const partial = named.find((entry) => entry.text.includes(normalizedLabel) || normalizedLabel.includes(entry.text));
|
|
2869
|
+
return partial?.candidate ?? null;
|
|
2870
|
+
}
|
|
2743
2871
|
/** Max settle-retry attempts for a primitive's DOM enumerate (see `pollEnumerate`). */
|
|
2744
2872
|
const PRIMITIVE_ENUMERATE_ATTEMPTS = 5;
|
|
2745
2873
|
/** Delay between settle-retry attempts. Total cap ≈ ATTEMPTS × this. */
|
|
@@ -4414,25 +4542,63 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4414
4542
|
}
|
|
4415
4543
|
}
|
|
4416
4544
|
/**
|
|
4417
|
-
*
|
|
4418
|
-
*
|
|
4419
|
-
* `
|
|
4420
|
-
*
|
|
4421
|
-
*
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4545
|
+
* Resolves deepLocator candidates scoped to `INTERACTIVE_CANDIDATE_SELECTOR`,
|
|
4546
|
+
* widening once to the unscoped `"*"` hop when the scoped pass finds
|
|
4547
|
+
* nothing. `INTERACTIVE_CANDIDATE_SELECTOR` is what makes a dense OOPIF
|
|
4548
|
+
* form's candidate set tractable (371 nodes down to a handful — see
|
|
4549
|
+
* `deep-locator-candidates.ts`'s module docblock), but it over-narrows a
|
|
4550
|
+
* `div`/`span` tile that carries only a click handler — no `role`/
|
|
4551
|
+
* `tabindex` — so a scoped-empty result would otherwise strand an attempt
|
|
4552
|
+
* the pre-1.6.13 unscoped `"*"` hop still resolved. The widened pass only
|
|
4553
|
+
* runs when the scoped one returns zero, so the dense-form fast path
|
|
4554
|
+
* (`flow-runner.oopif-dense-form-budget.test.ts`) still costs a single
|
|
4555
|
+
* resolve. Returns the `innerSelector` actually used alongside the
|
|
4556
|
+
* candidates so a caller driving `clickDeepLocatorCandidate` re-derives the
|
|
4557
|
+
* SAME hop the candidates were ranked/indexed against.
|
|
4558
|
+
*/
|
|
4559
|
+
async function resolveDeepLocatorCandidatesWithWidening(page, frameSelector, instruction, timeoutOptions) {
|
|
4560
|
+
const scoped = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR, instruction, timeoutOptions);
|
|
4561
|
+
if (scoped.length > 0) {
|
|
4562
|
+
return { candidates: scoped, innerSelector: deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR };
|
|
4563
|
+
}
|
|
4564
|
+
const widened = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, "*", instruction, timeoutOptions);
|
|
4565
|
+
return { candidates: widened, innerSelector: "*" };
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Adapts `resolveDeepLocatorCandidatesWithWidening` results into
|
|
4569
|
+
* `Action`-shaped evidence for `rephraseWithLLM`, which only knows about
|
|
4570
|
+
* Stagehand's `Action` type. Never throws: `resolveDeepLocatorCandidates`
|
|
4571
|
+
* itself degrades to `[]` on a resolver failure, so this only feeds the
|
|
4572
|
+
* rephrase prompt richer evidence when available — a frame-scoped step
|
|
4573
|
+
* whose observe AND deepLocator (scoped, then widened) both come back
|
|
4574
|
+
* empty just gets the same `[]` evidence it would have gotten before this
|
|
4575
|
+
* fix.
|
|
4576
|
+
*
|
|
4577
|
+
* `instruction` is forwarded so the evidence list is ranked by relevance to
|
|
4578
|
+
* the step, same as the act path — an unranked `[]`-then-DOM-order list
|
|
4579
|
+
* would feed the rephrase LLM its worst evidence first instead of its best.
|
|
4580
|
+
* It's also what {@link resolveDeepLocatorActuation} reads to pick the
|
|
4581
|
+
* evidence's `method`/`arguments` — a rephrase evaluating a "Fill in the
|
|
4582
|
+
* First Name field with 'Reginald'" step should see fill/"Reginald"
|
|
4583
|
+
* evidence, not a hard-coded click, now that the cascade itself can
|
|
4584
|
+
* actuate fill/select. Omitted (the unfocused-observe evidence list) falls
|
|
4585
|
+
* back to `click` with no value, same as before this method inference
|
|
4586
|
+
* existed.
|
|
4424
4587
|
*
|
|
4425
|
-
*
|
|
4426
|
-
*
|
|
4427
|
-
*
|
|
4428
|
-
* worst evidence first instead of its best.
|
|
4588
|
+
* Takes the caller's already-resolved `frameTarget` so
|
|
4589
|
+
* `resolveDeepLocatorCandidates` uses the batched frame-scoped evaluate
|
|
4590
|
+
* instead of re-resolving it internally.
|
|
4429
4591
|
*/
|
|
4430
|
-
async function deepLocatorCandidatesAsActions(page,
|
|
4431
|
-
const candidates = await (
|
|
4592
|
+
async function deepLocatorCandidatesAsActions(page, frameTarget, instruction) {
|
|
4593
|
+
const { candidates } = await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, instruction, { frameTarget });
|
|
4594
|
+
const actuation = instruction
|
|
4595
|
+
? resolveDeepLocatorActuation(instruction)
|
|
4596
|
+
: { kind: "click" };
|
|
4432
4597
|
return candidates.map((c) => ({
|
|
4433
4598
|
selector: c.selector,
|
|
4434
4599
|
description: c.accessibleText || "(no accessible text)",
|
|
4435
|
-
method: "click",
|
|
4600
|
+
method: actuation.kind === "select" ? "selectOption" : actuation.kind === "fill" ? "fill" : "click",
|
|
4601
|
+
...(actuation.kind === "click" ? {} : { arguments: [actuation.value] }),
|
|
4436
4602
|
}));
|
|
4437
4603
|
}
|
|
4438
4604
|
/**
|
|
@@ -4479,7 +4645,16 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4479
4645
|
? await reresolveFrameTarget()
|
|
4480
4646
|
: frameTarget;
|
|
4481
4647
|
if (effectiveFrameTarget?.frame) {
|
|
4482
|
-
|
|
4648
|
+
// Pass the already-resolved `effectiveFrameTarget` so the batched
|
|
4649
|
+
// evaluate reuses it instead of `resolveDeepLocatorCandidates`
|
|
4650
|
+
// re-resolving via its own internal `resolveFrameTarget` fallback —
|
|
4651
|
+
// same "reuse the ambient target" rule as every other frame-scoped
|
|
4652
|
+
// call in this function. Innerselector stays "*": this probe only
|
|
4653
|
+
// answers "does this frame have any content", and batching already
|
|
4654
|
+
// makes that reachability check cheap (one evaluate over every
|
|
4655
|
+
// node) — scoping to interactive elements would false-negative a
|
|
4656
|
+
// frame with rendered content but no controls yet.
|
|
4657
|
+
const deepLocatorCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, effectiveFrameTarget.frameSelector, "*", undefined, { frameTarget: effectiveFrameTarget });
|
|
4483
4658
|
if (deepLocatorCandidates.length > 0) {
|
|
4484
4659
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: observe found 0 candidates (focused and unfocused) but deepLocator found ${deepLocatorCandidates.length} — treating as present (let cascade resolve)`);
|
|
4485
4660
|
return "present";
|
|
@@ -4499,7 +4674,7 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4499
4674
|
}
|
|
4500
4675
|
}
|
|
4501
4676
|
async function executeStepWithHealing(params) {
|
|
4502
|
-
const { stagehand, page, step, optional, upload, submitStep, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, logger, captureFn,
|
|
4677
|
+
const { stagehand, page, step, optional, upload, submitStep, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, logger, captureFn, uploadFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, } = params;
|
|
4503
4678
|
// Mutable (not the destructured const above) so a lost frame-attach race
|
|
4504
4679
|
// can be upgraded in place once the OOPIF attaches later in the cascade —
|
|
4505
4680
|
// see reresolveFrameTargetIfLost below. Every existing reference in this
|
|
@@ -4573,7 +4748,7 @@ async function executeStepWithHealing(params) {
|
|
|
4573
4748
|
page,
|
|
4574
4749
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
4575
4750
|
isUploadStep: upload,
|
|
4576
|
-
fixture:
|
|
4751
|
+
fixture: uploadFixture,
|
|
4577
4752
|
logger,
|
|
4578
4753
|
signalCounter,
|
|
4579
4754
|
recentCaptureMeta,
|
|
@@ -4744,7 +4919,7 @@ async function executeStepWithHealing(params) {
|
|
|
4744
4919
|
// replan budget on every frame-scoped probe-absent failure.
|
|
4745
4920
|
await reresolveFrameTargetIfLost();
|
|
4746
4921
|
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
4747
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
4922
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
4748
4923
|
: probeAbsentObservedUnfocused;
|
|
4749
4924
|
const dumpPath = onStepFailure?.({
|
|
4750
4925
|
stepIndex,
|
|
@@ -4884,7 +5059,7 @@ async function executeStepWithHealing(params) {
|
|
|
4884
5059
|
if (attempt > 1) {
|
|
4885
5060
|
await page.waitForTimeout(attempt * ATTEMPT_BACKOFF_MS);
|
|
4886
5061
|
}
|
|
4887
|
-
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5062
|
+
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
4888
5063
|
// Snapshot the meta-tail length so the final-step pattern gate can scope
|
|
4889
5064
|
// its URL scan to captures added DURING this attempt (not historical
|
|
4890
5065
|
// tail from earlier steps).
|
|
@@ -5017,7 +5192,7 @@ async function executeStepWithHealing(params) {
|
|
|
5017
5192
|
// burn the step budget probing all of them.
|
|
5018
5193
|
const runnerUp = ranked[1];
|
|
5019
5194
|
if (runnerUp) {
|
|
5020
|
-
const midPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5195
|
+
const midPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5021
5196
|
const topVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
5022
5197
|
actResultSuccess: true,
|
|
5023
5198
|
pre,
|
|
@@ -5079,46 +5254,207 @@ async function executeStepWithHealing(params) {
|
|
|
5079
5254
|
// exclusion is applied here by filtering resolved (already-ranked)
|
|
5080
5255
|
// candidates against triedSelectors instead — otherwise attempt 4
|
|
5081
5256
|
// would re-pick the same failed element and burn the attempt.
|
|
5257
|
+
// `resolveDeepLocatorCandidatesWithWidening` widens the hop to `"*"`
|
|
5258
|
+
// once when the interactive-scoped pass finds nothing — a
|
|
5259
|
+
// `div`/`span` tile with only a click handler (no role/tabindex)
|
|
5260
|
+
// matches nothing scoped, so the widened pass is what still
|
|
5261
|
+
// surfaces it — and reports which innerSelector actually resolved
|
|
5262
|
+
// so the click below re-derives the same hop, not a stale one.
|
|
5082
5263
|
await reresolveFrameTargetIfLost();
|
|
5083
|
-
const
|
|
5084
|
-
?
|
|
5085
|
-
:
|
|
5264
|
+
const deepLocatorResolution = candidates.length === 0 && frameTarget?.frame
|
|
5265
|
+
? await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, step, { frameTarget })
|
|
5266
|
+
: null;
|
|
5267
|
+
const deepLocatorInnerSelector = deepLocatorResolution?.innerSelector ?? deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR;
|
|
5268
|
+
const deepLocatorCandidates = (deepLocatorResolution?.candidates ?? []).filter((c) => !triedSelectors.includes(c.selector));
|
|
5086
5269
|
if (candidates.length === 0 && deepLocatorCandidates.length > 0) {
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5270
|
+
// A fill/select step must actuate the NAMED FIELD through the
|
|
5271
|
+
// fill/select seam, never the click-only walk below: candidate
|
|
5272
|
+
// ranking scores by the instruction's quoted VALUE (see
|
|
5273
|
+
// parseFillStep's docblock) — for "Fill in the First Name field
|
|
5274
|
+
// with 'Reginald'" no control's accessible name is ever 'Reginald',
|
|
5275
|
+
// so every candidate ties at score 0 and the click walk would fire
|
|
5276
|
+
// on whatever sits first in DOM order (the bug report's wizard
|
|
5277
|
+
// 'Close' mis-click). Detect that shape first and route
|
|
5278
|
+
// deterministically to the candidate matching the field's own
|
|
5279
|
+
// label, refusing to click when no candidate names that field.
|
|
5280
|
+
const fillStep = parseFillStep(step);
|
|
5281
|
+
const selectStep = fillStep ? null : parseSelectStep(step);
|
|
5282
|
+
const fieldTarget = fillStep
|
|
5283
|
+
? { kind: "fill", fieldLabel: fillStep.fieldLabel, value: fillStep.value }
|
|
5284
|
+
: selectStep?.questionLabel
|
|
5285
|
+
? { kind: "select", fieldLabel: selectStep.questionLabel, value: selectStep.option }
|
|
5286
|
+
: null;
|
|
5287
|
+
if (fieldTarget) {
|
|
5288
|
+
const matched = findDeepLocatorCandidateByFieldLabel(deepLocatorCandidates, fieldTarget.fieldLabel);
|
|
5289
|
+
if (!matched) {
|
|
5290
|
+
const failureMessage = `deepLocator: no candidate matched field "${fieldTarget.fieldLabel}" (refusing to click an unrelated control)`;
|
|
5291
|
+
record.actResultSuccess = false;
|
|
5292
|
+
record.errorMessage = failureMessage;
|
|
5097
5293
|
attempts.push(record);
|
|
5098
|
-
failureReasons.push(
|
|
5099
|
-
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${
|
|
5294
|
+
failureReasons.push(failureMessage);
|
|
5295
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5100
5296
|
continue;
|
|
5101
5297
|
}
|
|
5102
|
-
triedSelectors.push(
|
|
5103
|
-
record.triedSelectors = [
|
|
5104
|
-
|
|
5105
|
-
await (0,
|
|
5298
|
+
triedSelectors.push(matched.selector);
|
|
5299
|
+
record.triedSelectors = [matched.selector];
|
|
5300
|
+
const actuated = fieldTarget.kind === "fill"
|
|
5301
|
+
? await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, matched.index, fieldTarget.value, { frameTarget })
|
|
5302
|
+
: await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, matched.index, fieldTarget.value, { frameTarget });
|
|
5303
|
+
if (actuated) {
|
|
5304
|
+
record.instruction = `deepLocator: ${matched.accessibleText || "(no accessible text)"}`;
|
|
5106
5305
|
record.actResultSuccess = true;
|
|
5107
|
-
record.actResultDescription = `deepLocator
|
|
5108
|
-
//
|
|
5109
|
-
//
|
|
5110
|
-
//
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5306
|
+
record.actResultDescription = `deepLocator ${fieldTarget.kind === "fill" ? "filled" : "selected"} "${matched.accessibleText || matched.selector}" with "${fieldTarget.value}"`;
|
|
5307
|
+
// verifyDomEffect can't resolve a `deeplocator=` selector (see
|
|
5308
|
+
// deep-locator-actuate.ts's module docblock: target.locator()
|
|
5309
|
+
// has no meaning for cross-origin OOPIF hop notation) — the
|
|
5310
|
+
// write + read-back fillDeepLocatorCandidate/
|
|
5311
|
+
// selectDeepLocatorCandidateOption already performed IS the
|
|
5312
|
+
// verification signal, so record it directly instead of
|
|
5313
|
+
// synthesizing a resolvedAction that a verifier can only ever
|
|
5314
|
+
// score false for.
|
|
5315
|
+
record.verifiedBy = "dom";
|
|
5316
|
+
attempts.push(record);
|
|
5317
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.actResultDescription}`);
|
|
5318
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
5319
|
+
return "completed";
|
|
5116
5320
|
}
|
|
5117
|
-
|
|
5321
|
+
const failureMessage = `deepLocator: ${fieldTarget.kind} on "${matched.accessibleText || matched.selector}" did not verify (read-back mismatch or rejected write)`;
|
|
5322
|
+
record.actResultSuccess = false;
|
|
5323
|
+
record.errorMessage = failureMessage;
|
|
5324
|
+
attempts.push(record);
|
|
5325
|
+
failureReasons.push(failureMessage);
|
|
5326
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5327
|
+
continue;
|
|
5328
|
+
}
|
|
5329
|
+
// A select step whose question is phrased un-quoted (parseSelectStep
|
|
5330
|
+
// returns `questionLabel: null`) has no fieldLabel to route through
|
|
5331
|
+
// findDeepLocatorCandidateByFieldLabel above, so ranking below falls
|
|
5332
|
+
// back to the OPTION value alone. Scoring the full interactive-scoped
|
|
5333
|
+
// `deepLocatorCandidates` here (buttons, inputs, links alongside any
|
|
5334
|
+
// <select>) would refuse on virtually every dense form: an unrelated
|
|
5335
|
+
// 'First Name' input or 'Submit Application' button never mentions
|
|
5336
|
+
// the quoted option any more than the genuine <select> target does,
|
|
5337
|
+
// so every one of them ties at score 0 right alongside it — not just
|
|
5338
|
+
// the two genuinely ambiguous <select>s the guard exists to catch.
|
|
5339
|
+
// A fresh resolve scoped to "select" narrows the tie check to only
|
|
5340
|
+
// the candidates that could actually satisfy a select-write (a
|
|
5341
|
+
// non-<select> candidate already reports "not-actionable" and is
|
|
5342
|
+
// skipped by the walk below regardless — see
|
|
5343
|
+
// buildSelectFrameCandidateExpr's `el.options || []` lookup), so
|
|
5344
|
+
// this only refuses when two (or more) real <select>s tie for
|
|
5345
|
+
// relevance, e.g. two 'Yes'/'No' controls whose own accessible names
|
|
5346
|
+
// never mention the option text. A unique top-ranked <select>
|
|
5347
|
+
// (including the common single-<select>-in-frame case, where a tie
|
|
5348
|
+
// is impossible) still reaches the walk.
|
|
5349
|
+
if (selectStep && !selectStep.questionLabel) {
|
|
5350
|
+
const selectCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget?.frameSelector, "select", null, { frameTarget });
|
|
5351
|
+
const optionPhrases = (0, deep_locator_candidates_1.extractTaggedPhrases)(step);
|
|
5352
|
+
const optionScores = selectCandidates.map((candidate) => (0, deep_locator_candidates_1.scoreCandidate)(candidate.accessibleText, optionPhrases));
|
|
5353
|
+
const topScore = optionScores.length > 0 ? Math.max(...optionScores) : 0;
|
|
5354
|
+
const tiedForTop = optionScores.filter((score) => score === topScore).length;
|
|
5355
|
+
if (tiedForTop > 1) {
|
|
5356
|
+
const failureMessage = `deepLocator: select step's question is un-quoted and ${tiedForTop} candidates tie for relevance to "${selectStep.option}" (refusing to guess which control to select)`;
|
|
5118
5357
|
record.actResultSuccess = false;
|
|
5119
|
-
record.errorMessage =
|
|
5358
|
+
record.errorMessage = failureMessage;
|
|
5359
|
+
attempts.push(record);
|
|
5360
|
+
failureReasons.push(failureMessage);
|
|
5361
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5362
|
+
continue;
|
|
5120
5363
|
}
|
|
5121
5364
|
}
|
|
5365
|
+
// Actionable-candidate walk: a top pick that rejects with the CDP
|
|
5366
|
+
// `-32000 Node does not have a layout object` error (an unrendered
|
|
5367
|
+
// node) or is refused by the wizard-exit deny-list costs only that
|
|
5368
|
+
// one candidate, not the whole attempt — the next ranked candidate
|
|
5369
|
+
// is tried instead. `attemptTriedSelectors` mirrors the walk's own
|
|
5370
|
+
// click attempts as they happen (not just on a successful return)
|
|
5371
|
+
// so a click that throws a REAL error (e.g. a wedged
|
|
5372
|
+
// `WatchdogTimeoutError`) still feeds attempt 4's exclusion filter.
|
|
5373
|
+
const attemptTriedSelectors = [];
|
|
5374
|
+
const denyCandidate = (candidate) => {
|
|
5375
|
+
const denied = isWizardExitAction(candidate.accessibleText, wizardExitButtonLabels);
|
|
5376
|
+
if (denied) {
|
|
5377
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: refused wizard-exit control: "${candidate.accessibleText.slice(0, 60)}"`);
|
|
5378
|
+
}
|
|
5379
|
+
return denied;
|
|
5380
|
+
};
|
|
5381
|
+
// Intent discrimination: observe()'s Stagehand-resolved action
|
|
5382
|
+
// carries its own method + fill/select arguments (see attempt 1's
|
|
5383
|
+
// `target.method === "fill"` handling above); the deepLocator
|
|
5384
|
+
// fallback has no such resolved action, so the step prose is the
|
|
5385
|
+
// only place the fill/select value can come from. Derived once per
|
|
5386
|
+
// attempt (not per candidate) — every candidate in this walk is a
|
|
5387
|
+
// guess at the SAME step's target, so they all actuate the same way.
|
|
5388
|
+
const actuation = resolveDeepLocatorActuation(step);
|
|
5389
|
+
const cascadeResult = await (0, deep_locator_click_1.clickFirstActionableCandidate)(deepLocatorCandidates, async (candidate) => {
|
|
5390
|
+
attemptTriedSelectors.push(candidate.selector);
|
|
5391
|
+
if (actuation.kind === "select") {
|
|
5392
|
+
const verified = await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5393
|
+
// selectDeepLocatorCandidateOption never throws on an ordinary
|
|
5394
|
+
// failed write (see deep-locator-actuate.ts's writeAndVerify) —
|
|
5395
|
+
// it resolves `false` for both a rejected selectOption() and a
|
|
5396
|
+
// read-back mismatch. clickFirstActionableCandidate infers
|
|
5397
|
+
// success from "didn't throw", so a `false` here must become a
|
|
5398
|
+
// throw or the walk would wrongly report this candidate as
|
|
5399
|
+
// actuated.
|
|
5400
|
+
if (!verified)
|
|
5401
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5402
|
+
}
|
|
5403
|
+
else if (actuation.kind === "fill") {
|
|
5404
|
+
const verified = await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5405
|
+
if (!verified)
|
|
5406
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5407
|
+
}
|
|
5408
|
+
else {
|
|
5409
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, { frameTarget });
|
|
5410
|
+
}
|
|
5411
|
+
}, { denyCandidate })
|
|
5412
|
+
.then((outcome) => ({ outcome, error: null }))
|
|
5413
|
+
.catch((error) => ({ outcome: null, error }));
|
|
5414
|
+
triedSelectors.push(...attemptTriedSelectors);
|
|
5415
|
+
record.triedSelectors = [...attemptTriedSelectors];
|
|
5416
|
+
if (cascadeResult.outcome?.clicked && cascadeResult.outcome.candidate) {
|
|
5417
|
+
const acted = cascadeResult.outcome.candidate;
|
|
5418
|
+
const verb = actuation.kind === "select"
|
|
5419
|
+
? "selected"
|
|
5420
|
+
: actuation.kind === "fill"
|
|
5421
|
+
? "filled"
|
|
5422
|
+
: "clicked";
|
|
5423
|
+
record.instruction = `deepLocator: ${acted.accessibleText || "(no accessible text)"}`;
|
|
5424
|
+
record.actResultSuccess = true;
|
|
5425
|
+
record.actResultDescription = `deepLocator ${verb} "${acted.accessibleText || acted.selector}"`;
|
|
5426
|
+
// Synthesize an action matching the actuation kind so downstream
|
|
5427
|
+
// verification (network/url/dom — STATE_CLASS_METHODS treats
|
|
5428
|
+
// fill/selectOption as DOM-verifiable, not click) treats this
|
|
5429
|
+
// exactly like any other resolved action — same idiom as
|
|
5430
|
+
// deep-submit-locator/structured-click.
|
|
5431
|
+
resolvedAction =
|
|
5432
|
+
actuation.kind === "click"
|
|
5433
|
+
? {
|
|
5434
|
+
selector: acted.selector,
|
|
5435
|
+
description: record.actResultDescription,
|
|
5436
|
+
method: "click",
|
|
5437
|
+
}
|
|
5438
|
+
: {
|
|
5439
|
+
selector: acted.selector,
|
|
5440
|
+
description: record.actResultDescription,
|
|
5441
|
+
method: actuation.kind === "select" ? "selectOption" : "fill",
|
|
5442
|
+
arguments: [actuation.value],
|
|
5443
|
+
};
|
|
5444
|
+
}
|
|
5445
|
+
else {
|
|
5446
|
+
const failureMessage = cascadeResult.error
|
|
5447
|
+
? `deepLocator: ${actuation.kind} threw ${(0, errors_1.toErrorMessage)(cascadeResult.error)}`
|
|
5448
|
+
: attemptTriedSelectors.length > 0
|
|
5449
|
+
? `deepLocator: no actionable candidate (${attemptTriedSelectors.length} not-actionable)`
|
|
5450
|
+
: "deepLocator: no actionable candidate (every candidate refused by the wizard-exit deny-list)";
|
|
5451
|
+
record.actResultSuccess = false;
|
|
5452
|
+
record.errorMessage = failureMessage;
|
|
5453
|
+
attempts.push(record);
|
|
5454
|
+
failureReasons.push(failureMessage);
|
|
5455
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5456
|
+
continue;
|
|
5457
|
+
}
|
|
5122
5458
|
}
|
|
5123
5459
|
else if (candidates.length === 0) {
|
|
5124
5460
|
record.errorMessage = "observe returned no candidates";
|
|
@@ -5395,7 +5731,7 @@ async function executeStepWithHealing(params) {
|
|
|
5395
5731
|
// prompt, so a resolver error/empty result is fine to swallow.
|
|
5396
5732
|
await reresolveFrameTargetIfLost();
|
|
5397
5733
|
const candidates = observedCandidates.length === 0 && frameTarget?.frame
|
|
5398
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
5734
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget, step)
|
|
5399
5735
|
: observedCandidates;
|
|
5400
5736
|
// Fetch live-page evidence so the rephrase prompt can reason about
|
|
5401
5737
|
// form state, not just observe candidates. Mirrors the same
|
|
@@ -5413,7 +5749,7 @@ async function executeStepWithHealing(params) {
|
|
|
5413
5749
|
// alongside the failed step, not in the top document.
|
|
5414
5750
|
const observedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5415
5751
|
const unfocused = observedUnfocused.length === 0 && frameTarget?.frame
|
|
5416
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
5752
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
5417
5753
|
: observedUnfocused;
|
|
5418
5754
|
const submitFailureList = extractSubmitFailureEvidence(recentCaptures, ownBackendHostnames);
|
|
5419
5755
|
const gaEventList = extractGaEventEvidence(recentCaptures);
|
|
@@ -5466,7 +5802,7 @@ async function executeStepWithHealing(params) {
|
|
|
5466
5802
|
continue;
|
|
5467
5803
|
}
|
|
5468
5804
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
5469
|
-
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5805
|
+
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5470
5806
|
record.post = post;
|
|
5471
5807
|
if (resolvedAction) {
|
|
5472
5808
|
record.resolvedMethod = resolvedAction.method ?? null;
|
|
@@ -5541,7 +5877,19 @@ async function executeStepWithHealing(params) {
|
|
|
5541
5877
|
if (domVerified && !domVerifiedForStep) {
|
|
5542
5878
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} advance step succeeded only via DOM state change (field toggle / non-advancing POST), not a real transition; not treating as verified`);
|
|
5543
5879
|
}
|
|
5544
|
-
|
|
5880
|
+
// Client-side view-swap gate: credit a click that produces substantial
|
|
5881
|
+
// DOM growth (≥5KB) with zero network when it's NOT a submit/final step
|
|
5882
|
+
// and NOT an advance-pattern step. Fixes the UCHealth "Manual Application"
|
|
5883
|
+
// case where a +49KB DOM-only view swap was scored as "no observable effect".
|
|
5884
|
+
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
5885
|
+
resolvedAction,
|
|
5886
|
+
isFinalStep,
|
|
5887
|
+
submitStep,
|
|
5888
|
+
isAdvanceWithPattern: isAdvanceStep(step) && advanceTransitionBodyPattern !== null,
|
|
5889
|
+
networkDelta: post.networkCount - pre.networkCount,
|
|
5890
|
+
bytesDelta: post.bodyHtmlLength - pre.bodyHtmlLength,
|
|
5891
|
+
});
|
|
5892
|
+
let verified = networkIsRealAdvance || urlChanged || domVerifiedForStep || clickViewSwapVerified;
|
|
5545
5893
|
// Final-step submit-verification gate. Replaces the deterministic
|
|
5546
5894
|
// submitEndpointPattern regex with a Haiku 4.5 LLM judgment over
|
|
5547
5895
|
// multi-signal evidence (network captures, page URL/title, DOM
|
|
@@ -5654,7 +6002,13 @@ async function executeStepWithHealing(params) {
|
|
|
5654
6002
|
if (verified && record.verifiedBy === null) {
|
|
5655
6003
|
// Preserve a richer verdict set earlier in this attempt (e.g.
|
|
5656
6004
|
// "submitted-state-dom" from the final-step DOM fallback).
|
|
5657
|
-
record.verifiedBy =
|
|
6005
|
+
record.verifiedBy = clickViewSwapVerified
|
|
6006
|
+
? "view-swap"
|
|
6007
|
+
: urlChanged
|
|
6008
|
+
? "url"
|
|
6009
|
+
: networkFired
|
|
6010
|
+
? "network"
|
|
6011
|
+
: "dom";
|
|
5658
6012
|
}
|
|
5659
6013
|
// N+16 probe: Stagehand's CDP click sometimes lands on the button without
|
|
5660
6014
|
// triggering React's SyntheticEvent layer (or jQuery delegated handlers).
|
|
@@ -5736,7 +6090,7 @@ async function executeStepWithHealing(params) {
|
|
|
5736
6090
|
probeResult.checked === true &&
|
|
5737
6091
|
!ancestorStillInvalid;
|
|
5738
6092
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
5739
|
-
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
6093
|
+
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5740
6094
|
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
5741
6095
|
const retryUrlChanged = retryPost.url !== pre.url;
|
|
5742
6096
|
const retryHtmlDelta = retryPost.bodyHtmlLength - pre.bodyHtmlLength;
|
|
@@ -6035,7 +6389,7 @@ async function executeStepWithHealing(params) {
|
|
|
6035
6389
|
// replan budget on every frame-scoped cascade-exhaust failure.
|
|
6036
6390
|
await reresolveFrameTargetIfLost();
|
|
6037
6391
|
const finalObserve = cascadeExhaustObservedFinal.length === 0 && frameTarget?.frame
|
|
6038
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
6392
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget, step)
|
|
6039
6393
|
: cascadeExhaustObservedFinal;
|
|
6040
6394
|
const { pageTitle, pageUrl } = await resolveDumpPageIdentity(page, frameTarget);
|
|
6041
6395
|
// Discriminator data for "Stagehand sees nothing" failures: capture the raw
|
|
@@ -6047,7 +6401,7 @@ async function executeStepWithHealing(params) {
|
|
|
6047
6401
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
6048
6402
|
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
6049
6403
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
6050
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
6404
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
6051
6405
|
: cascadeExhaustObservedUnfocused;
|
|
6052
6406
|
const dumpPath = onStepFailure?.({
|
|
6053
6407
|
stepIndex,
|
|
@@ -6134,7 +6488,7 @@ async function waitForSpaReady(page, logger, opts = {}) {
|
|
|
6134
6488
|
* than let the flow report phantom success.
|
|
6135
6489
|
*/
|
|
6136
6490
|
async function runHealingFlow(deps) {
|
|
6137
|
-
const { stagehand, page, steps, logger, anthropic,
|
|
6491
|
+
const { stagehand, page, steps, logger, anthropic, uploadFixture, maxFlowMs } = deps;
|
|
6138
6492
|
const counter = { n: 0 };
|
|
6139
6493
|
const signalCounter = { n: 0 };
|
|
6140
6494
|
const recentCaptures = [];
|
|
@@ -6187,7 +6541,7 @@ async function runHealingFlow(deps) {
|
|
|
6187
6541
|
recentCaptureMeta,
|
|
6188
6542
|
anthropic,
|
|
6189
6543
|
logger,
|
|
6190
|
-
|
|
6544
|
+
uploadFixture,
|
|
6191
6545
|
frameTarget,
|
|
6192
6546
|
isFinalStep: i === steps.length - 1,
|
|
6193
6547
|
submitEndpointPattern: deps.submitEndpointPattern ?? null,
|