@enricai/barnacle 1.6.8 → 1.6.10
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/deep-locator-candidates.d.ts +62 -0
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -0
- package/dist/scraper/deep-locator-candidates.js +160 -0
- package/dist/scraper/deep-locator-candidates.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +88 -0
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -0
- package/dist/scraper/deep-locator-fake.js +86 -0
- package/dist/scraper/deep-locator-fake.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +8 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +152 -26
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +23 -13
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +73 -25
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +15 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +19 -3
- package/dist/scripts/recon-browser.js.map +1 -1
- package/package.json +1 -1
|
@@ -87,6 +87,7 @@ const schemas_1 = require("../lib/llm/schemas");
|
|
|
87
87
|
const logging_1 = require("../lib/logging");
|
|
88
88
|
const call_capture_1 = require("../lib/telemetry/call-capture");
|
|
89
89
|
const call_types_1 = require("../lib/telemetry/call-types");
|
|
90
|
+
const deep_locator_candidates_1 = require("../scraper/deep-locator-candidates");
|
|
90
91
|
const errors_2 = require("../scraper/errors");
|
|
91
92
|
const frame_target_1 = require("../scraper/frame-target");
|
|
92
93
|
const phantom_click_1 = require("../scraper/phantom-click");
|
|
@@ -4408,6 +4409,28 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4408
4409
|
return [];
|
|
4409
4410
|
}
|
|
4410
4411
|
}
|
|
4412
|
+
/**
|
|
4413
|
+
* Adapts `resolveDeepLocatorCandidates` results into `Action`-shaped
|
|
4414
|
+
* evidence for `rephraseWithLLM`, which only knows about Stagehand's
|
|
4415
|
+
* `Action` type. Never throws: `resolveDeepLocatorCandidates` itself
|
|
4416
|
+
* degrades to `[]` on a resolver failure, so this only feeds the rephrase
|
|
4417
|
+
* prompt richer evidence when available — a frame-scoped step whose
|
|
4418
|
+
* observe AND deepLocator both come back empty just gets the same `[]`
|
|
4419
|
+
* evidence it would have gotten before this fix.
|
|
4420
|
+
*
|
|
4421
|
+
* `instruction` is forwarded to `resolveDeepLocatorCandidates` so the
|
|
4422
|
+
* evidence list is ranked by relevance to the step, same as the act path —
|
|
4423
|
+
* an unranked `[]`-then-DOM-order list would feed the rephrase LLM its
|
|
4424
|
+
* worst evidence first instead of its best.
|
|
4425
|
+
*/
|
|
4426
|
+
async function deepLocatorCandidatesAsActions(page, frameSelector, instruction) {
|
|
4427
|
+
const candidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, "*", instruction);
|
|
4428
|
+
return candidates.map((c) => ({
|
|
4429
|
+
selector: c.selector,
|
|
4430
|
+
description: c.accessibleText || "(no accessible text)",
|
|
4431
|
+
method: "click",
|
|
4432
|
+
}));
|
|
4433
|
+
}
|
|
4411
4434
|
/**
|
|
4412
4435
|
* Cheap pre-cascade reachability gate. Runs before the 5-attempt healing cascade
|
|
4413
4436
|
* (and any global replan) so a step aimed at the wrong page state fails fast
|
|
@@ -4419,10 +4442,16 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4419
4442
|
*
|
|
4420
4443
|
* `frameTarget` scopes both observe calls to a resolved cross-origin child
|
|
4421
4444
|
* frame when the flow declared `frameSelector`; omitted (main frame) is
|
|
4422
|
-
* byte-identical to today's unscoped calls.
|
|
4445
|
+
* byte-identical to today's unscoped calls. When `frameTarget.frame` is a
|
|
4446
|
+
* resolved child frame, `observe()` is blind to it (measured against a
|
|
4447
|
+
* cross-origin OOPIF — see `deep-locator-candidates.ts`'s module docblock),
|
|
4448
|
+
* so a 0-candidate focused+unfocused observe pair additionally falls back to
|
|
4449
|
+
* `resolveDeepLocatorCandidates` before declaring the step "absent" — a
|
|
4450
|
+
* frame-scoped step must not short-circuit to replan before the cascade
|
|
4451
|
+
* (which itself now routes through the same resolver) ever runs.
|
|
4423
4452
|
*/
|
|
4424
4453
|
async function probeStepBeforeAttempts(params) {
|
|
4425
|
-
const { stagehand, step, stepIndex, totalSteps, logger, captureFn, frameTarget } = params;
|
|
4454
|
+
const { stagehand, page, step, stepIndex, totalSteps, logger, captureFn, frameTarget } = params;
|
|
4426
4455
|
try {
|
|
4427
4456
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
4428
4457
|
if (candidates.length === 0) {
|
|
@@ -4442,6 +4471,13 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4442
4471
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: focused probe found 0 candidates but unfocused observe found ${unfocused.length} — treating as present (let cascade resolve)`);
|
|
4443
4472
|
return "present";
|
|
4444
4473
|
}
|
|
4474
|
+
if (frameTarget?.frame) {
|
|
4475
|
+
const deepLocatorCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget.frameSelector, "*");
|
|
4476
|
+
if (deepLocatorCandidates.length > 0) {
|
|
4477
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: observe found 0 candidates (focused and unfocused) but deepLocator found ${deepLocatorCandidates.length} — treating as present (let cascade resolve)`);
|
|
4478
|
+
return "present";
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4445
4481
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: probe found 0 candidates (focused and unfocused) — treating as absent (skip cascade, route to replan if required)`);
|
|
4446
4482
|
return "absent";
|
|
4447
4483
|
}
|
|
@@ -4490,7 +4526,7 @@ async function executeStepWithHealing(params) {
|
|
|
4490
4526
|
// the existing cascade.
|
|
4491
4527
|
if (await tryUploadPrimitive({
|
|
4492
4528
|
page,
|
|
4493
|
-
target:
|
|
4529
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
4494
4530
|
isUploadStep: upload,
|
|
4495
4531
|
fixture: resumeFixture,
|
|
4496
4532
|
logger,
|
|
@@ -4508,11 +4544,10 @@ async function executeStepWithHealing(params) {
|
|
|
4508
4544
|
// question unanswered. No-op (returns false → falls through) when the step
|
|
4509
4545
|
// isn't a single-dropdown select or no option matches.
|
|
4510
4546
|
//
|
|
4511
|
-
//
|
|
4512
|
-
// when no frameSelector is set, so this
|
|
4513
|
-
//
|
|
4514
|
-
|
|
4515
|
-
const selectFrameTarget = await (0, frame_target_1.resolveFrameTarget)(page);
|
|
4547
|
+
// Reuse the already-resolved ambient frameTarget rather than re-resolving:
|
|
4548
|
+
// falls back to the main-frame target when no frameSelector is set, so this
|
|
4549
|
+
// bridge stays behavior-identical for every existing site.
|
|
4550
|
+
const selectFrameTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
4516
4551
|
if (await trySelectPrimitive({
|
|
4517
4552
|
page,
|
|
4518
4553
|
target: selectFrameTarget,
|
|
@@ -4588,6 +4623,7 @@ async function executeStepWithHealing(params) {
|
|
|
4588
4623
|
// burning 4 attempts on a page that clearly isn't the right one.
|
|
4589
4624
|
const probeResult = await probeStepBeforeAttempts({
|
|
4590
4625
|
stagehand,
|
|
4626
|
+
page,
|
|
4591
4627
|
step,
|
|
4592
4628
|
stepIndex,
|
|
4593
4629
|
totalSteps,
|
|
@@ -4651,7 +4687,15 @@ async function executeStepWithHealing(params) {
|
|
|
4651
4687
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
4652
4688
|
.catch(() => null);
|
|
4653
4689
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
4654
|
-
const
|
|
4690
|
+
const probeAbsentObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
4691
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
4692
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
4693
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
4694
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
4695
|
+
// replan budget on every frame-scoped probe-absent failure.
|
|
4696
|
+
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
4697
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
4698
|
+
: probeAbsentObservedUnfocused;
|
|
4655
4699
|
const dumpPath = onStepFailure?.({
|
|
4656
4700
|
stepIndex,
|
|
4657
4701
|
phase,
|
|
@@ -4689,7 +4733,7 @@ async function executeStepWithHealing(params) {
|
|
|
4689
4733
|
: 0;
|
|
4690
4734
|
if (requireSubmitEndpoint) {
|
|
4691
4735
|
const invalidControls = await probeFormValidityBeforeSubmit({
|
|
4692
|
-
target:
|
|
4736
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
4693
4737
|
stepIndex,
|
|
4694
4738
|
totalSteps,
|
|
4695
4739
|
logger,
|
|
@@ -4974,7 +5018,58 @@ async function executeStepWithHealing(params) {
|
|
|
4974
5018
|
? { ignoreSelectors: [...triedSelectors], timeout: exports.STEP_WATCHDOG_MS }
|
|
4975
5019
|
: { timeout: exports.STEP_WATCHDOG_MS };
|
|
4976
5020
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, observeOptions, captureFn, frameTarget);
|
|
4977
|
-
|
|
5021
|
+
// observe() is blind to a cross-origin OOPIF (see
|
|
5022
|
+
// deep-locator-candidates.ts's module docblock) — when the step is
|
|
5023
|
+
// frame-scoped and observe came back empty, fall back to the deep
|
|
5024
|
+
// locator resolver before declaring the attempt candidate-less.
|
|
5025
|
+
// Passing `step` ranks candidates by relevance to the instruction
|
|
5026
|
+
// (see resolveDeepLocatorCandidates) so the top pick below is the
|
|
5027
|
+
// element the step actually names, not just DOM order.
|
|
5028
|
+
// Attempt 4's ignoreSelectors has no deepLocator equivalent, so the
|
|
5029
|
+
// exclusion is applied here by filtering resolved (already-ranked)
|
|
5030
|
+
// candidates against triedSelectors instead — otherwise attempt 4
|
|
5031
|
+
// would re-pick the same failed element and burn the attempt.
|
|
5032
|
+
const deepLocatorCandidates = candidates.length === 0 && frameTarget?.frame
|
|
5033
|
+
? (await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget.frameSelector, "*", step)).filter((c) => !triedSelectors.includes(c.selector))
|
|
5034
|
+
: [];
|
|
5035
|
+
if (candidates.length === 0 && deepLocatorCandidates.length > 0) {
|
|
5036
|
+
const top = deepLocatorCandidates[0];
|
|
5037
|
+
if (top) {
|
|
5038
|
+
record.instruction = `deepLocator: ${top.accessibleText || "(no accessible text)"}`;
|
|
5039
|
+
// Deny-list guard: mirrors the observe branch's refusal below —
|
|
5040
|
+
// never act on a wizard-exit control regardless of which
|
|
5041
|
+
// candidate source (observe vs. deepLocator) surfaced it.
|
|
5042
|
+
if (isWizardExitAction(top.accessibleText, wizardExitButtonLabels)) {
|
|
5043
|
+
record.errorMessage = `refused wizard-exit control: "${top.accessibleText.slice(0, 60)}"`;
|
|
5044
|
+
triedSelectors.push(top.selector);
|
|
5045
|
+
record.triedSelectors = [top.selector];
|
|
5046
|
+
attempts.push(record);
|
|
5047
|
+
failureReasons.push(record.errorMessage);
|
|
5048
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.errorMessage}`);
|
|
5049
|
+
continue;
|
|
5050
|
+
}
|
|
5051
|
+
triedSelectors.push(top.selector);
|
|
5052
|
+
record.triedSelectors = [top.selector];
|
|
5053
|
+
try {
|
|
5054
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, "*", top.index);
|
|
5055
|
+
record.actResultSuccess = true;
|
|
5056
|
+
record.actResultDescription = `deepLocator clicked "${top.accessibleText || top.selector}"`;
|
|
5057
|
+
// Synthesize a click action so downstream verification (network
|
|
5058
|
+
// / url / dom) treats this exactly like any other resolved
|
|
5059
|
+
// click — same idiom as deep-submit-locator/structured-click.
|
|
5060
|
+
resolvedAction = {
|
|
5061
|
+
selector: top.selector,
|
|
5062
|
+
description: record.actResultDescription,
|
|
5063
|
+
method: "click",
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
catch (err) {
|
|
5067
|
+
record.actResultSuccess = false;
|
|
5068
|
+
record.errorMessage = `deepLocator: click threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
else if (candidates.length === 0) {
|
|
4978
5073
|
record.errorMessage = "observe returned no candidates";
|
|
4979
5074
|
// Optional-step short-circuit: when attempt 2 confirms no candidates
|
|
4980
5075
|
// match AND the step was marked optional in the flow, skip cleanly.
|
|
@@ -5051,8 +5146,8 @@ async function executeStepWithHealing(params) {
|
|
|
5051
5146
|
target.arguments.length > 0) {
|
|
5052
5147
|
const fillValue = target.arguments[0];
|
|
5053
5148
|
if (typeof fillValue === "string") {
|
|
5054
|
-
const
|
|
5055
|
-
const dateFill = await fillHtml5DateTimeInput(
|
|
5149
|
+
const dateFillTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
5150
|
+
const dateFill = await fillHtml5DateTimeInput(dateFillTarget, target.selector, fillValue);
|
|
5056
5151
|
if (dateFill !== null) {
|
|
5057
5152
|
record.errorMessage = dateFill.filled
|
|
5058
5153
|
? `html5-date-fallback: filled ${dateFill.inputType}="${dateFill.postValue}"`
|
|
@@ -5072,7 +5167,7 @@ async function executeStepWithHealing(params) {
|
|
|
5072
5167
|
// component rejection, masked-input library reformatting).
|
|
5073
5168
|
// Generic primitive that the verifier's existing signals
|
|
5074
5169
|
// (network/url/dom/htmlDelta/textChanged) miss.
|
|
5075
|
-
const readback = await verifyFillReadback(
|
|
5170
|
+
const readback = await verifyFillReadback(dateFillTarget, target.selector, fillValue);
|
|
5076
5171
|
if (readback !== null) {
|
|
5077
5172
|
if (readback.outcome === "rejected") {
|
|
5078
5173
|
record.errorMessage = `fill-value-rejected: tried "${fillValue.slice(0, 60)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
@@ -5190,7 +5285,7 @@ async function executeStepWithHealing(params) {
|
|
|
5190
5285
|
return { resolved: true, isCheckable: true, checked: false, strategyUsed: null };
|
|
5191
5286
|
})()`;
|
|
5192
5287
|
try {
|
|
5193
|
-
const structuredClickTarget =
|
|
5288
|
+
const structuredClickTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
5194
5289
|
const result = await structuredClickTarget.evaluate(probeExpr);
|
|
5195
5290
|
if (result !== null && typeof result === "object" && "resolved" in result) {
|
|
5196
5291
|
const probe = result;
|
|
@@ -5241,7 +5336,15 @@ async function executeStepWithHealing(params) {
|
|
|
5241
5336
|
"anthropic billing exhausted (FATAL_BILLING already logged); skipping rephrase";
|
|
5242
5337
|
}
|
|
5243
5338
|
else {
|
|
5244
|
-
const
|
|
5339
|
+
const observedCandidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5340
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped
|
|
5341
|
+
// empty result degrades to the deep-locator resolver for prompt
|
|
5342
|
+
// evidence rather than hard-failing — lower stakes than the
|
|
5343
|
+
// attempt-2/4 click path since this only feeds the rephrase
|
|
5344
|
+
// prompt, so a resolver error/empty result is fine to swallow.
|
|
5345
|
+
const candidates = observedCandidates.length === 0 && frameTarget?.frame
|
|
5346
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
5347
|
+
: observedCandidates;
|
|
5245
5348
|
// Fetch live-page evidence so the rephrase prompt can reason about
|
|
5246
5349
|
// form state, not just observe candidates. Mirrors the same
|
|
5247
5350
|
// extraction the cascade-exhaust dump path already does.
|
|
@@ -5252,8 +5355,14 @@ async function executeStepWithHealing(params) {
|
|
|
5252
5355
|
});
|
|
5253
5356
|
// Unfocused observe so the rephrase prompt can see ambient UI
|
|
5254
5357
|
// like modal Save/Close buttons that the focused candidates
|
|
5255
|
-
// (filtered by the failed step's instruction) would hide.
|
|
5256
|
-
|
|
5358
|
+
// (filtered by the failed step's instruction) would hide. Scoped to
|
|
5359
|
+
// frameTarget: for a frame-scoped flow the ambient UI that matters
|
|
5360
|
+
// (the wizard's own Save/Close controls) lives inside the iframe
|
|
5361
|
+
// alongside the failed step, not in the top document.
|
|
5362
|
+
const observedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5363
|
+
const unfocused = observedUnfocused.length === 0 && frameTarget?.frame
|
|
5364
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
5365
|
+
: observedUnfocused;
|
|
5257
5366
|
const submitFailureList = extractSubmitFailureEvidence(recentCaptures, ownBackendHostnames);
|
|
5258
5367
|
const gaEventList = extractGaEventEvidence(recentCaptures);
|
|
5259
5368
|
const priorAttemptsForPrompt = attempts.map((a, i) => ({
|
|
@@ -5322,7 +5431,7 @@ async function executeStepWithHealing(params) {
|
|
|
5322
5431
|
// false for non-radio/non-checkbox clicks so the network/URL signal still
|
|
5323
5432
|
// decides those. Radios/checkboxes are click-but-no-network just like fills.
|
|
5324
5433
|
const domVerified = resolvedAction !== null && (isStateClass || isClick)
|
|
5325
|
-
? await verifyDomEffect(
|
|
5434
|
+
? await verifyDomEffect(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction)
|
|
5326
5435
|
: false;
|
|
5327
5436
|
// Interior-advance transition gate (opt-in). On SPAs where a page advance
|
|
5328
5437
|
// and a mere field-edit share one endpoint URL (the wizard ATS's `/gq`:
|
|
@@ -5415,7 +5524,7 @@ async function executeStepWithHealing(params) {
|
|
|
5415
5524
|
return null;
|
|
5416
5525
|
})()`;
|
|
5417
5526
|
try {
|
|
5418
|
-
const submittedStateTarget =
|
|
5527
|
+
const submittedStateTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
5419
5528
|
domSubmittedMatch = (await submittedStateTarget.evaluate(probeExpr));
|
|
5420
5529
|
}
|
|
5421
5530
|
catch (err) {
|
|
@@ -5424,7 +5533,7 @@ async function executeStepWithHealing(params) {
|
|
|
5424
5533
|
}
|
|
5425
5534
|
// Build the unfocused-observe evidence list. Used by the judge to
|
|
5426
5535
|
// assess whether the page transitioned to a success state.
|
|
5427
|
-
const unfocusedForJudge = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn).catch(() => []);
|
|
5536
|
+
const unfocusedForJudge = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5428
5537
|
// Quick invalid-marker count (deterministic DOM querying — counting
|
|
5429
5538
|
// structural ng-invalid containers is not fuzzy matching, just
|
|
5430
5539
|
// observing existence).
|
|
@@ -5540,7 +5649,7 @@ async function executeStepWithHealing(params) {
|
|
|
5540
5649
|
// reliably trigger that default action — same gap N+42 documented
|
|
5541
5650
|
// for direct checkbox/radio clicks.
|
|
5542
5651
|
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; 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 }; } el.click(); return { fired: true, kind: "click" }; })()`;
|
|
5543
|
-
const n16FallbackTarget =
|
|
5652
|
+
const n16FallbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
5544
5653
|
const probeResult = (await n16FallbackTarget.evaluate(clickExpr));
|
|
5545
5654
|
const fired = probeResult.fired;
|
|
5546
5655
|
// Vacuous-click guard for the n+16 fallback. Same rationale as
|
|
@@ -5860,7 +5969,15 @@ async function executeStepWithHealing(params) {
|
|
|
5860
5969
|
}
|
|
5861
5970
|
}
|
|
5862
5971
|
}
|
|
5863
|
-
const
|
|
5972
|
+
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5973
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
5974
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
5975
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
5976
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
5977
|
+
// replan budget on every frame-scoped cascade-exhaust failure.
|
|
5978
|
+
const finalObserve = cascadeExhaustObservedFinal.length === 0 && frameTarget?.frame
|
|
5979
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
5980
|
+
: cascadeExhaustObservedFinal;
|
|
5864
5981
|
const { pageTitle, pageUrl } = await resolveDumpPageIdentity(page, frameTarget);
|
|
5865
5982
|
// Discriminator data for "Stagehand sees nothing" failures: capture the raw
|
|
5866
5983
|
// DOM and an unfocused observe so a triager can tell empty-page from
|
|
@@ -5869,7 +5986,10 @@ async function executeStepWithHealing(params) {
|
|
|
5869
5986
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
5870
5987
|
.catch(() => null);
|
|
5871
5988
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
5872
|
-
const
|
|
5989
|
+
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5990
|
+
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
5991
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
5992
|
+
: cascadeExhaustObservedUnfocused;
|
|
5873
5993
|
const dumpPath = onStepFailure?.({
|
|
5874
5994
|
stepIndex,
|
|
5875
5995
|
phase,
|
|
@@ -5951,8 +6071,6 @@ async function runHealingFlow(deps) {
|
|
|
5951
6071
|
let submitVerified = false;
|
|
5952
6072
|
let submitStepSkipped = false;
|
|
5953
6073
|
let lastStepIndex = -1;
|
|
5954
|
-
const frameTarget = await (0, frame_target_1.resolveFrameTarget)(page, deps.frameSelector);
|
|
5955
|
-
await (0, frame_target_1.waitForChildFrameReady)(frameTarget);
|
|
5956
6074
|
const stopCapture = wireSignalCapture(page, {
|
|
5957
6075
|
counter,
|
|
5958
6076
|
signalCounter,
|
|
@@ -5974,6 +6092,14 @@ async function runHealingFlow(deps) {
|
|
|
5974
6092
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(i, () => steps.length)} flow exceeded its maxFlowMs budget (${maxFlowMs}ms)`, "flow-timeout");
|
|
5975
6093
|
}
|
|
5976
6094
|
lastStepIndex = i;
|
|
6095
|
+
// Resolved fresh per step (not cached across the run) so a cross-origin
|
|
6096
|
+
// iframe that attaches mid-flow (e.g. after an "Apply" click reveals a
|
|
6097
|
+
// wizard embedded later in the DOM) is picked up as soon as it's
|
|
6098
|
+
// reachable, mirroring the recon CLI's per-step resolution. `resolveFrameTarget`
|
|
6099
|
+
// falls back to the main-frame target when `frameSelector` is null/unresolvable,
|
|
6100
|
+
// so this is a no-op for every flow that doesn't declare one.
|
|
6101
|
+
const frameTarget = await (0, frame_target_1.resolveFrameTarget)(page, deps.frameSelector);
|
|
6102
|
+
await (0, frame_target_1.waitForChildFrameReady)(frameTarget);
|
|
5977
6103
|
const outcome = await executeStepWithHealing({
|
|
5978
6104
|
stagehand,
|
|
5979
6105
|
page,
|