@enricai/barnacle 1.6.9 → 1.6.11
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 +71 -3
- package/dist/api/routes/submissions.d.ts +21 -0
- package/dist/api/routes/submissions.d.ts.map +1 -0
- package/dist/api/routes/submissions.js +48 -0
- package/dist/api/routes/submissions.js.map +1 -0
- package/dist/api/schemas/submissions.d.ts +110 -0
- package/dist/api/schemas/submissions.d.ts.map +1 -0
- package/dist/api/schemas/submissions.js +77 -0
- package/dist/api/schemas/submissions.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/telemetry/beacon-capture.d.ts +37 -0
- package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -0
- package/dist/lib/telemetry/beacon-capture.js +47 -0
- package/dist/lib/telemetry/beacon-capture.js.map +1 -0
- package/dist/lib/telemetry/reconciliation-record.d.ts +105 -0
- package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -0
- package/dist/lib/telemetry/reconciliation-record.js +76 -0
- package/dist/lib/telemetry/reconciliation-record.js.map +1 -0
- package/dist/lib/telemetry/reconciliation-source.d.ts +33 -0
- package/dist/lib/telemetry/reconciliation-source.d.ts.map +1 -0
- package/dist/lib/telemetry/reconciliation-source.js +91 -0
- package/dist/lib/telemetry/reconciliation-source.js.map +1 -0
- package/dist/lib/telemetry/s3-sink.d.ts.map +1 -1
- package/dist/lib/telemetry/s3-sink.js +1 -2
- package/dist/lib/telemetry/s3-sink.js.map +1 -1
- package/dist/lib/telemetry/submission-capture.d.ts +30 -9
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-capture.js +32 -21
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/lib/telemetry/submission-query.d.ts +32 -0
- package/dist/lib/telemetry/submission-query.d.ts.map +1 -0
- package/dist/lib/telemetry/submission-query.js +49 -0
- package/dist/lib/telemetry/submission-query.js.map +1 -0
- package/dist/lib/telemetry/submission-reader.d.ts +52 -0
- package/dist/lib/telemetry/submission-reader.d.ts.map +1 -0
- package/dist/lib/telemetry/submission-reader.js +111 -0
- package/dist/lib/telemetry/submission-reader.js.map +1 -0
- package/dist/lib/telemetry/submissions-s3-objects.d.ts +23 -0
- package/dist/lib/telemetry/submissions-s3-objects.d.ts.map +1 -0
- package/dist/lib/telemetry/submissions-s3-objects.js +132 -0
- package/dist/lib/telemetry/submissions-s3-objects.js.map +1 -0
- package/dist/lib/telemetry/submissions-s3-reader.d.ts +25 -0
- package/dist/lib/telemetry/submissions-s3-reader.d.ts.map +1 -0
- package/dist/lib/telemetry/submissions-s3-reader.js +63 -0
- package/dist/lib/telemetry/submissions-s3-reader.js.map +1 -0
- package/dist/lib/tracking-click.d.ts +13 -2
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +42 -4
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/plugins/loader.d.ts +17 -3
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +59 -6
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/cookie-jar.d.ts +10 -4
- package/dist/scraper/cookie-jar.d.ts.map +1 -1
- package/dist/scraper/cookie-jar.js +10 -5
- package/dist/scraper/cookie-jar.js.map +1 -1
- package/dist/scraper/deep-locator-candidates.d.ts +84 -0
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -0
- package/dist/scraper/deep-locator-candidates.js +206 -0
- package/dist/scraper/deep-locator-candidates.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +116 -0
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -0
- package/dist/scraper/deep-locator-fake.js +128 -0
- package/dist/scraper/deep-locator-fake.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +28 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +204 -14
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +60 -14
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +126 -38
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scraper/stagehand-guard.d.ts +14 -0
- package/dist/scraper/stagehand-guard.d.ts.map +1 -1
- package/dist/scraper/stagehand-guard.js +28 -8
- package/dist/scraper/stagehand-guard.js.map +1 -1
- package/dist/scraper/watchdog.d.ts +38 -0
- package/dist/scraper/watchdog.d.ts.map +1 -0
- package/dist/scraper/watchdog.js +54 -0
- package/dist/scraper/watchdog.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +14 -3
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/site-plugin.d.ts +12 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -87,11 +87,13 @@ 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");
|
|
93
94
|
const stagehand_guard_1 = require("../scraper/stagehand-guard");
|
|
94
95
|
const submit_control_1 = require("../scraper/submit-control");
|
|
96
|
+
const watchdog_1 = require("../scraper/watchdog");
|
|
95
97
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
96
98
|
const logger = (0, logging_1.getLogger)({ name: "scraper/flow-runner" });
|
|
97
99
|
/** Cap on the rolling capture-filename window held in memory for failure dumps. */
|
|
@@ -521,9 +523,12 @@ async function snapshotPage(target, signalCounter) {
|
|
|
521
523
|
* body HTML and url instead of pairing a child frame's DOM with the top
|
|
522
524
|
* document's url. `title()` intentionally still reads the top document for
|
|
523
525
|
* a child frame (see `frame-target.ts`); `url()` is the frame discriminator.
|
|
526
|
+
* Routed through `mainFrameTarget(page)` (rather than a raw `page.title()`)
|
|
527
|
+
* when no `frameTarget` is resolved so this CDP read is watchdog-bounded the
|
|
528
|
+
* same way a resolved `FrameTarget`'s `title()` already is.
|
|
524
529
|
*/
|
|
525
530
|
async function resolveDumpPageIdentity(page, frameTarget) {
|
|
526
|
-
const pageTitle = await (frameTarget ?? page).title().catch(() => "");
|
|
531
|
+
const pageTitle = await (frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).title().catch(() => "");
|
|
527
532
|
const pageUrl = await (frameTarget ? frameTarget.url() : Promise.resolve(page.url())).catch(() => page.url());
|
|
528
533
|
return { pageTitle, pageUrl };
|
|
529
534
|
}
|
|
@@ -4408,6 +4413,28 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4408
4413
|
return [];
|
|
4409
4414
|
}
|
|
4410
4415
|
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Adapts `resolveDeepLocatorCandidates` results into `Action`-shaped
|
|
4418
|
+
* evidence for `rephraseWithLLM`, which only knows about Stagehand's
|
|
4419
|
+
* `Action` type. Never throws: `resolveDeepLocatorCandidates` itself
|
|
4420
|
+
* degrades to `[]` on a resolver failure, so this only feeds the rephrase
|
|
4421
|
+
* prompt richer evidence when available — a frame-scoped step whose
|
|
4422
|
+
* observe AND deepLocator both come back empty just gets the same `[]`
|
|
4423
|
+
* evidence it would have gotten before this fix.
|
|
4424
|
+
*
|
|
4425
|
+
* `instruction` is forwarded to `resolveDeepLocatorCandidates` so the
|
|
4426
|
+
* evidence list is ranked by relevance to the step, same as the act path —
|
|
4427
|
+
* an unranked `[]`-then-DOM-order list would feed the rephrase LLM its
|
|
4428
|
+
* worst evidence first instead of its best.
|
|
4429
|
+
*/
|
|
4430
|
+
async function deepLocatorCandidatesAsActions(page, frameSelector, instruction) {
|
|
4431
|
+
const candidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, "*", instruction);
|
|
4432
|
+
return candidates.map((c) => ({
|
|
4433
|
+
selector: c.selector,
|
|
4434
|
+
description: c.accessibleText || "(no accessible text)",
|
|
4435
|
+
method: "click",
|
|
4436
|
+
}));
|
|
4437
|
+
}
|
|
4411
4438
|
/**
|
|
4412
4439
|
* Cheap pre-cascade reachability gate. Runs before the 5-attempt healing cascade
|
|
4413
4440
|
* (and any global replan) so a step aimed at the wrong page state fails fast
|
|
@@ -4419,10 +4446,16 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4419
4446
|
*
|
|
4420
4447
|
* `frameTarget` scopes both observe calls to a resolved cross-origin child
|
|
4421
4448
|
* frame when the flow declared `frameSelector`; omitted (main frame) is
|
|
4422
|
-
* byte-identical to today's unscoped calls.
|
|
4449
|
+
* byte-identical to today's unscoped calls. When `frameTarget.frame` is a
|
|
4450
|
+
* resolved child frame, `observe()` is blind to it (measured against a
|
|
4451
|
+
* cross-origin OOPIF — see `deep-locator-candidates.ts`'s module docblock),
|
|
4452
|
+
* so a 0-candidate focused+unfocused observe pair additionally falls back to
|
|
4453
|
+
* `resolveDeepLocatorCandidates` before declaring the step "absent" — a
|
|
4454
|
+
* frame-scoped step must not short-circuit to replan before the cascade
|
|
4455
|
+
* (which itself now routes through the same resolver) ever runs.
|
|
4423
4456
|
*/
|
|
4424
4457
|
async function probeStepBeforeAttempts(params) {
|
|
4425
|
-
const { stagehand, step, stepIndex, totalSteps, logger, captureFn, frameTarget } = params;
|
|
4458
|
+
const { stagehand, page, step, stepIndex, totalSteps, logger, captureFn, frameTarget, reresolveFrameTarget, } = params;
|
|
4426
4459
|
try {
|
|
4427
4460
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
4428
4461
|
if (candidates.length === 0) {
|
|
@@ -4442,6 +4475,16 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4442
4475
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: focused probe found 0 candidates but unfocused observe found ${unfocused.length} — treating as present (let cascade resolve)`);
|
|
4443
4476
|
return "present";
|
|
4444
4477
|
}
|
|
4478
|
+
const effectiveFrameTarget = reresolveFrameTarget
|
|
4479
|
+
? await reresolveFrameTarget()
|
|
4480
|
+
: frameTarget;
|
|
4481
|
+
if (effectiveFrameTarget?.frame) {
|
|
4482
|
+
const deepLocatorCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, effectiveFrameTarget.frameSelector, "*");
|
|
4483
|
+
if (deepLocatorCandidates.length > 0) {
|
|
4484
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: observe found 0 candidates (focused and unfocused) but deepLocator found ${deepLocatorCandidates.length} — treating as present (let cascade resolve)`);
|
|
4485
|
+
return "present";
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4445
4488
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: probe found 0 candidates (focused and unfocused) — treating as absent (skip cascade, route to replan if required)`);
|
|
4446
4489
|
return "absent";
|
|
4447
4490
|
}
|
|
@@ -4456,7 +4499,45 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4456
4499
|
}
|
|
4457
4500
|
}
|
|
4458
4501
|
async function executeStepWithHealing(params) {
|
|
4459
|
-
const { stagehand, page,
|
|
4502
|
+
const { stagehand, page, step, optional, upload, submitStep, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, logger, captureFn, resumeFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, } = params;
|
|
4503
|
+
// Mutable (not the destructured const above) so a lost frame-attach race
|
|
4504
|
+
// can be upgraded in place once the OOPIF attaches later in the cascade —
|
|
4505
|
+
// see reresolveFrameTargetIfLost below. Every existing reference in this
|
|
4506
|
+
// function keeps reading `frameTarget` unchanged; only its declaration
|
|
4507
|
+
// moved so it can be reassigned.
|
|
4508
|
+
let frameTarget = params.frameTarget;
|
|
4509
|
+
let frameReresolveAttempted = false;
|
|
4510
|
+
/**
|
|
4511
|
+
* Re-resolves a frame that lost the attach race at step entry —
|
|
4512
|
+
* `resolveFrameTarget`'s per-step poll (called once in the runner's step
|
|
4513
|
+
* loop, before `executeStepWithHealing` even starts) gives up and pins
|
|
4514
|
+
* `frameTarget.frame` to `null` for the rest of the step, so every
|
|
4515
|
+
* deepLocator gate below tests a stale "unresolved" handle even after the
|
|
4516
|
+
* OOPIF actually attaches moments later (the run 5 vs. run 6 divergence in
|
|
4517
|
+
* the bug report). Called just before each gate; re-checks with
|
|
4518
|
+
* `timeoutMs: 0` — a single presence probe, no added poll delay — so a
|
|
4519
|
+
* frame that has since attached is picked up without making the deadline
|
|
4520
|
+
* that already elapsed matter again. Memoized (`frameReresolveAttempted`)
|
|
4521
|
+
* so the cascade's several gates share one attempt instead of each
|
|
4522
|
+
* re-checking; a target with no `declaredFrameSelector` (the frame was
|
|
4523
|
+
* never declared, or resolution already succeeded) returns immediately
|
|
4524
|
+
* without calling `resolveFrameTarget` at all, so main-frame-only flows do
|
|
4525
|
+
* zero extra work.
|
|
4526
|
+
*/
|
|
4527
|
+
const reresolveFrameTargetIfLost = async () => {
|
|
4528
|
+
if (frameReresolveAttempted)
|
|
4529
|
+
return;
|
|
4530
|
+
frameReresolveAttempted = true;
|
|
4531
|
+
if (frameTarget?.frame || !frameTarget?.declaredFrameSelector)
|
|
4532
|
+
return;
|
|
4533
|
+
const reresolved = await (0, frame_target_1.resolveFrameTarget)(page, frameTarget.declaredFrameSelector, {
|
|
4534
|
+
timeoutMs: 0,
|
|
4535
|
+
});
|
|
4536
|
+
if (!reresolved.frame)
|
|
4537
|
+
return;
|
|
4538
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} frame ${frameTarget.declaredFrameSelector} attached after step entry — re-resolved before deepLocator gate`);
|
|
4539
|
+
frameTarget = reresolved;
|
|
4540
|
+
};
|
|
4460
4541
|
// Read-once to suppress "unused" — knownErrorClassPrefixes is threaded
|
|
4461
4542
|
// through executeStepWithHealing's signature so the cascade has it in
|
|
4462
4543
|
// scope when the invalid-fields judge migration (Task #43) lands. The
|
|
@@ -4587,12 +4668,17 @@ async function executeStepWithHealing(params) {
|
|
|
4587
4668
|
// burning 4 attempts on a page that clearly isn't the right one.
|
|
4588
4669
|
const probeResult = await probeStepBeforeAttempts({
|
|
4589
4670
|
stagehand,
|
|
4671
|
+
page,
|
|
4590
4672
|
step,
|
|
4591
4673
|
stepIndex,
|
|
4592
4674
|
totalSteps,
|
|
4593
4675
|
logger,
|
|
4594
4676
|
captureFn,
|
|
4595
4677
|
frameTarget,
|
|
4678
|
+
reresolveFrameTarget: async () => {
|
|
4679
|
+
await reresolveFrameTargetIfLost();
|
|
4680
|
+
return frameTarget;
|
|
4681
|
+
},
|
|
4596
4682
|
});
|
|
4597
4683
|
if (probeResult === "absent") {
|
|
4598
4684
|
if (optional) {
|
|
@@ -4650,7 +4736,16 @@ async function executeStepWithHealing(params) {
|
|
|
4650
4736
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
4651
4737
|
.catch(() => null);
|
|
4652
4738
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
4653
|
-
const
|
|
4739
|
+
const probeAbsentObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
4740
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
4741
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
4742
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
4743
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
4744
|
+
// replan budget on every frame-scoped probe-absent failure.
|
|
4745
|
+
await reresolveFrameTargetIfLost();
|
|
4746
|
+
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
4747
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
4748
|
+
: probeAbsentObservedUnfocused;
|
|
4654
4749
|
const dumpPath = onStepFailure?.({
|
|
4655
4750
|
stepIndex,
|
|
4656
4751
|
phase,
|
|
@@ -4973,7 +5068,59 @@ async function executeStepWithHealing(params) {
|
|
|
4973
5068
|
? { ignoreSelectors: [...triedSelectors], timeout: exports.STEP_WATCHDOG_MS }
|
|
4974
5069
|
: { timeout: exports.STEP_WATCHDOG_MS };
|
|
4975
5070
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, observeOptions, captureFn, frameTarget);
|
|
4976
|
-
|
|
5071
|
+
// observe() is blind to a cross-origin OOPIF (see
|
|
5072
|
+
// deep-locator-candidates.ts's module docblock) — when the step is
|
|
5073
|
+
// frame-scoped and observe came back empty, fall back to the deep
|
|
5074
|
+
// locator resolver before declaring the attempt candidate-less.
|
|
5075
|
+
// Passing `step` ranks candidates by relevance to the instruction
|
|
5076
|
+
// (see resolveDeepLocatorCandidates) so the top pick below is the
|
|
5077
|
+
// element the step actually names, not just DOM order.
|
|
5078
|
+
// Attempt 4's ignoreSelectors has no deepLocator equivalent, so the
|
|
5079
|
+
// exclusion is applied here by filtering resolved (already-ranked)
|
|
5080
|
+
// candidates against triedSelectors instead — otherwise attempt 4
|
|
5081
|
+
// would re-pick the same failed element and burn the attempt.
|
|
5082
|
+
await reresolveFrameTargetIfLost();
|
|
5083
|
+
const deepLocatorCandidates = candidates.length === 0 && frameTarget?.frame
|
|
5084
|
+
? (await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget.frameSelector, "*", step)).filter((c) => !triedSelectors.includes(c.selector))
|
|
5085
|
+
: [];
|
|
5086
|
+
if (candidates.length === 0 && deepLocatorCandidates.length > 0) {
|
|
5087
|
+
const top = deepLocatorCandidates[0];
|
|
5088
|
+
if (top) {
|
|
5089
|
+
record.instruction = `deepLocator: ${top.accessibleText || "(no accessible text)"}`;
|
|
5090
|
+
// Deny-list guard: mirrors the observe branch's refusal below —
|
|
5091
|
+
// never act on a wizard-exit control regardless of which
|
|
5092
|
+
// candidate source (observe vs. deepLocator) surfaced it.
|
|
5093
|
+
if (isWizardExitAction(top.accessibleText, wizardExitButtonLabels)) {
|
|
5094
|
+
record.errorMessage = `refused wizard-exit control: "${top.accessibleText.slice(0, 60)}"`;
|
|
5095
|
+
triedSelectors.push(top.selector);
|
|
5096
|
+
record.triedSelectors = [top.selector];
|
|
5097
|
+
attempts.push(record);
|
|
5098
|
+
failureReasons.push(record.errorMessage);
|
|
5099
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.errorMessage}`);
|
|
5100
|
+
continue;
|
|
5101
|
+
}
|
|
5102
|
+
triedSelectors.push(top.selector);
|
|
5103
|
+
record.triedSelectors = [top.selector];
|
|
5104
|
+
try {
|
|
5105
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, "*", top.index);
|
|
5106
|
+
record.actResultSuccess = true;
|
|
5107
|
+
record.actResultDescription = `deepLocator clicked "${top.accessibleText || top.selector}"`;
|
|
5108
|
+
// Synthesize a click action so downstream verification (network
|
|
5109
|
+
// / url / dom) treats this exactly like any other resolved
|
|
5110
|
+
// click — same idiom as deep-submit-locator/structured-click.
|
|
5111
|
+
resolvedAction = {
|
|
5112
|
+
selector: top.selector,
|
|
5113
|
+
description: record.actResultDescription,
|
|
5114
|
+
method: "click",
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
5117
|
+
catch (err) {
|
|
5118
|
+
record.actResultSuccess = false;
|
|
5119
|
+
record.errorMessage = `deepLocator: click threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
else if (candidates.length === 0) {
|
|
4977
5124
|
record.errorMessage = "observe returned no candidates";
|
|
4978
5125
|
// Optional-step short-circuit: when attempt 2 confirms no candidates
|
|
4979
5126
|
// match AND the step was marked optional in the flow, skip cleanly.
|
|
@@ -5240,7 +5387,16 @@ async function executeStepWithHealing(params) {
|
|
|
5240
5387
|
"anthropic billing exhausted (FATAL_BILLING already logged); skipping rephrase";
|
|
5241
5388
|
}
|
|
5242
5389
|
else {
|
|
5243
|
-
const
|
|
5390
|
+
const observedCandidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5391
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped
|
|
5392
|
+
// empty result degrades to the deep-locator resolver for prompt
|
|
5393
|
+
// evidence rather than hard-failing — lower stakes than the
|
|
5394
|
+
// attempt-2/4 click path since this only feeds the rephrase
|
|
5395
|
+
// prompt, so a resolver error/empty result is fine to swallow.
|
|
5396
|
+
await reresolveFrameTargetIfLost();
|
|
5397
|
+
const candidates = observedCandidates.length === 0 && frameTarget?.frame
|
|
5398
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
5399
|
+
: observedCandidates;
|
|
5244
5400
|
// Fetch live-page evidence so the rephrase prompt can reason about
|
|
5245
5401
|
// form state, not just observe candidates. Mirrors the same
|
|
5246
5402
|
// extraction the cascade-exhaust dump path already does.
|
|
@@ -5255,7 +5411,10 @@ async function executeStepWithHealing(params) {
|
|
|
5255
5411
|
// frameTarget: for a frame-scoped flow the ambient UI that matters
|
|
5256
5412
|
// (the wizard's own Save/Close controls) lives inside the iframe
|
|
5257
5413
|
// alongside the failed step, not in the top document.
|
|
5258
|
-
const
|
|
5414
|
+
const observedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5415
|
+
const unfocused = observedUnfocused.length === 0 && frameTarget?.frame
|
|
5416
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
5417
|
+
: observedUnfocused;
|
|
5259
5418
|
const submitFailureList = extractSubmitFailureEvidence(recentCaptures, ownBackendHostnames);
|
|
5260
5419
|
const gaEventList = extractGaEventEvidence(recentCaptures);
|
|
5261
5420
|
const priorAttemptsForPrompt = attempts.map((a, i) => ({
|
|
@@ -5431,7 +5590,10 @@ async function executeStepWithHealing(params) {
|
|
|
5431
5590
|
// structural ng-invalid containers is not fuzzy matching, just
|
|
5432
5591
|
// observing existence).
|
|
5433
5592
|
const invalidMarkerCount = await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).catch(() => 0);
|
|
5434
|
-
const pageTitle = await
|
|
5593
|
+
const pageTitle = await (0, watchdog_1.withWatchdog)(() => page.title(), {
|
|
5594
|
+
timeoutMs: config_1.config.scraper.frameEvaluateTimeoutMs,
|
|
5595
|
+
label: "flow-runner: submit-verification page title probe",
|
|
5596
|
+
}).catch(() => "");
|
|
5435
5597
|
const matchedSubmittedSelectors = domSubmittedMatch !== null ? [domSubmittedMatch] : [];
|
|
5436
5598
|
const judgeVerdict = await (0, verify_submit_1.verifySubmitWithLLM)({
|
|
5437
5599
|
client: anthropic,
|
|
@@ -5664,7 +5826,10 @@ async function executeStepWithHealing(params) {
|
|
|
5664
5826
|
}
|
|
5665
5827
|
const unfocusedForJudge = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5666
5828
|
const invalidMarkerCount = await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).catch(() => 0);
|
|
5667
|
-
const pageTitle = await
|
|
5829
|
+
const pageTitle = await (0, watchdog_1.withWatchdog)(() => page.title(), {
|
|
5830
|
+
timeoutMs: config_1.config.scraper.frameEvaluateTimeoutMs,
|
|
5831
|
+
label: "flow-runner: submit-verification retry page title probe",
|
|
5832
|
+
}).catch(() => "");
|
|
5668
5833
|
const matchedSubmittedSelectors = domSubmittedMatch !== null ? [domSubmittedMatch] : [];
|
|
5669
5834
|
const judgeVerdict = await (0, verify_submit_1.verifySubmitWithLLM)({
|
|
5670
5835
|
client: anthropic,
|
|
@@ -5862,16 +6027,28 @@ async function executeStepWithHealing(params) {
|
|
|
5862
6027
|
}
|
|
5863
6028
|
}
|
|
5864
6029
|
}
|
|
5865
|
-
const
|
|
6030
|
+
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
6031
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
6032
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
6033
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
6034
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
6035
|
+
// replan budget on every frame-scoped cascade-exhaust failure.
|
|
6036
|
+
await reresolveFrameTargetIfLost();
|
|
6037
|
+
const finalObserve = cascadeExhaustObservedFinal.length === 0 && frameTarget?.frame
|
|
6038
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
6039
|
+
: cascadeExhaustObservedFinal;
|
|
5866
6040
|
const { pageTitle, pageUrl } = await resolveDumpPageIdentity(page, frameTarget);
|
|
5867
6041
|
// Discriminator data for "Stagehand sees nothing" failures: capture the raw
|
|
5868
6042
|
// DOM and an unfocused observe so a triager can tell empty-page from
|
|
5869
6043
|
// Stagehand-can't-see-it without reproducing the failure.
|
|
5870
|
-
const bodyOuterHtmlRaw = await (frameTarget ?? page)
|
|
6044
|
+
const bodyOuterHtmlRaw = await (frameTarget ?? (0, frame_target_1.mainFrameTarget)(page))
|
|
5871
6045
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
5872
6046
|
.catch(() => null);
|
|
5873
6047
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
5874
|
-
const
|
|
6048
|
+
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
6049
|
+
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
6050
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
6051
|
+
: cascadeExhaustObservedUnfocused;
|
|
5875
6052
|
const dumpPath = onStepFailure?.({
|
|
5876
6053
|
stepIndex,
|
|
5877
6054
|
phase,
|
|
@@ -5902,6 +6079,16 @@ const SPA_MIN_BODY_LENGTH = 5_000;
|
|
|
5902
6079
|
* the entire flow. The recon CLI has this gate inline; generated plugins call it
|
|
5903
6080
|
* here so they inherit the same behavior. Polls `document.body.outerHTML.length`
|
|
5904
6081
|
* up to a threshold, then proceeds regardless (best-effort, never throws).
|
|
6082
|
+
*
|
|
6083
|
+
* Each `document.body.outerHTML.length` read is itself bounded by a watchdog
|
|
6084
|
+
* (capped to one poll interval) and treated as "still 0 chars" on timeout —
|
|
6085
|
+
* same pattern as `waitForChildFrameReady`'s `isReady()` probe. Without this,
|
|
6086
|
+
* a single wedged CDP round-trip inside `readBodyLength` would pend forever
|
|
6087
|
+
* and the `while (Date.now() < deadline)` loop below would never be
|
|
6088
|
+
* re-entered, defeating the very deadline it exists to enforce.
|
|
6089
|
+
* `page.waitForTimeout(pollMs)` is left unguarded: it is a plain delay (no
|
|
6090
|
+
* DOM/network read whose response can be lost), so it does not carry the
|
|
6091
|
+
* "wedged read" failure mode this fix targets.
|
|
5905
6092
|
*/
|
|
5906
6093
|
async function waitForSpaReady(page, logger, opts = {}) {
|
|
5907
6094
|
const timeoutMs = opts.timeoutMs ?? SPA_READINESS_TIMEOUT_MS;
|
|
@@ -5909,7 +6096,10 @@ async function waitForSpaReady(page, logger, opts = {}) {
|
|
|
5909
6096
|
const minBodyLength = opts.minBodyLength ?? SPA_MIN_BODY_LENGTH;
|
|
5910
6097
|
const bodyLengthExpr = "document.body ? document.body.outerHTML.length : 0";
|
|
5911
6098
|
const readBodyLength = async () => {
|
|
5912
|
-
const raw = await
|
|
6099
|
+
const raw = await (0, watchdog_1.withWatchdog)(() => page.evaluate(bodyLengthExpr), {
|
|
6100
|
+
timeoutMs: pollMs,
|
|
6101
|
+
label: "flow-runner: spa readiness body-length probe",
|
|
6102
|
+
}).catch(() => 0);
|
|
5913
6103
|
return typeof raw === "number" ? raw : 0;
|
|
5914
6104
|
};
|
|
5915
6105
|
let bodyLength = await readBodyLength();
|