@mercuryo-ai/agentbrowse 0.2.57 → 0.2.61

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.
Files changed (100) hide show
  1. package/README.md +76 -57
  2. package/dist/browser-session-state.d.ts +39 -0
  3. package/dist/browser-session-state.d.ts.map +1 -1
  4. package/dist/browser-session-state.js +63 -1
  5. package/dist/command-name.js +1 -1
  6. package/dist/commands/act.d.ts.map +1 -1
  7. package/dist/commands/act.js +540 -528
  8. package/dist/commands/action-executor-helpers.d.ts.map +1 -1
  9. package/dist/commands/action-executor-helpers.js +10 -8
  10. package/dist/commands/attach.d.ts.map +1 -1
  11. package/dist/commands/attach.js +5 -10
  12. package/dist/commands/browser-connection-failure.d.ts +9 -0
  13. package/dist/commands/browser-connection-failure.d.ts.map +1 -0
  14. package/dist/commands/browser-connection-failure.js +15 -0
  15. package/dist/commands/browser-status.d.ts.map +1 -1
  16. package/dist/commands/browser-status.js +26 -30
  17. package/dist/commands/click-activation-policy.d.ts.map +1 -1
  18. package/dist/commands/click-activation-policy.js +6 -2
  19. package/dist/commands/close.d.ts.map +1 -1
  20. package/dist/commands/close.js +5 -0
  21. package/dist/commands/extract.d.ts.map +1 -1
  22. package/dist/commands/extract.js +147 -144
  23. package/dist/commands/launch.d.ts +0 -1
  24. package/dist/commands/launch.d.ts.map +1 -1
  25. package/dist/commands/launch.js +13 -16
  26. package/dist/commands/navigate.d.ts.map +1 -1
  27. package/dist/commands/navigate.js +79 -73
  28. package/dist/commands/observe-inventory.d.ts +6 -1
  29. package/dist/commands/observe-inventory.d.ts.map +1 -1
  30. package/dist/commands/observe-inventory.js +331 -8
  31. package/dist/commands/observe-persistence.d.ts.map +1 -1
  32. package/dist/commands/observe-persistence.js +2 -0
  33. package/dist/commands/observe-projection.d.ts +3 -2
  34. package/dist/commands/observe-projection.d.ts.map +1 -1
  35. package/dist/commands/observe-projection.js +1 -0
  36. package/dist/commands/observe-protected.d.ts +3 -1
  37. package/dist/commands/observe-protected.d.ts.map +1 -1
  38. package/dist/commands/observe-protected.js +23 -1
  39. package/dist/commands/observe-semantics.d.ts.map +1 -1
  40. package/dist/commands/observe-semantics.js +70 -0
  41. package/dist/commands/observe.d.ts +1 -0
  42. package/dist/commands/observe.d.ts.map +1 -1
  43. package/dist/commands/observe.js +260 -270
  44. package/dist/commands/screenshot.d.ts.map +1 -1
  45. package/dist/commands/screenshot.js +50 -64
  46. package/dist/control-semantics.d.ts.map +1 -1
  47. package/dist/control-semantics.js +5 -0
  48. package/dist/date-value-normalization.d.ts +16 -0
  49. package/dist/date-value-normalization.d.ts.map +1 -0
  50. package/dist/date-value-normalization.js +117 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -24
  53. package/dist/library.d.ts +5 -1
  54. package/dist/library.d.ts.map +1 -1
  55. package/dist/library.js +4 -1
  56. package/dist/protected-fill.d.ts +3 -2
  57. package/dist/protected-fill.d.ts.map +1 -1
  58. package/dist/protected-fill.js +46 -7
  59. package/dist/runtime-protected-state.d.ts.map +1 -1
  60. package/dist/runtime-protected-state.js +8 -1
  61. package/dist/runtime-state.d.ts +11 -0
  62. package/dist/runtime-state.d.ts.map +1 -1
  63. package/dist/secrets/form-matcher.d.ts +1 -2
  64. package/dist/secrets/form-matcher.d.ts.map +1 -1
  65. package/dist/secrets/form-matcher.js +125 -119
  66. package/dist/secrets/matching-helpers.d.ts +13 -0
  67. package/dist/secrets/matching-helpers.d.ts.map +1 -0
  68. package/dist/secrets/matching-helpers.js +147 -0
  69. package/dist/secrets/observed-field-resolution.d.ts +43 -0
  70. package/dist/secrets/observed-field-resolution.d.ts.map +1 -0
  71. package/dist/secrets/observed-field-resolution.js +223 -0
  72. package/dist/secrets/protected-field-semantics.d.ts.map +1 -1
  73. package/dist/secrets/protected-field-semantics.js +3 -2
  74. package/dist/secrets/protected-fill.d.ts +3 -1
  75. package/dist/secrets/protected-fill.d.ts.map +1 -1
  76. package/dist/secrets/protected-fill.js +31 -0
  77. package/dist/secrets/protected-value-adapters.d.ts.map +1 -1
  78. package/dist/secrets/protected-value-adapters.js +14 -22
  79. package/dist/secrets/types.d.ts +3 -0
  80. package/dist/secrets/types.d.ts.map +1 -1
  81. package/dist/sticky-owner-host-entry.d.ts +2 -0
  82. package/dist/sticky-owner-host-entry.d.ts.map +1 -0
  83. package/dist/sticky-owner-host-entry.js +97 -0
  84. package/dist/sticky-owner.d.ts +15 -0
  85. package/dist/sticky-owner.d.ts.map +1 -0
  86. package/dist/sticky-owner.js +431 -0
  87. package/docs/README.md +15 -2
  88. package/docs/api-reference.md +13 -3
  89. package/docs/assistive-runtime.md +63 -7
  90. package/docs/configuration.md +48 -8
  91. package/docs/getting-started.md +42 -9
  92. package/docs/integration-checklist.md +8 -7
  93. package/docs/protected-fill.md +40 -7
  94. package/docs/testing.md +4 -3
  95. package/docs/troubleshooting.md +126 -36
  96. package/examples/README.md +9 -2
  97. package/package.json +8 -3
  98. package/dist/protected-fill-browser.d.ts +0 -22
  99. package/dist/protected-fill-browser.d.ts.map +0 -1
  100. package/dist/protected-fill-browser.js +0 -52
@@ -5,7 +5,7 @@ import { saveSession } from '../session.js';
5
5
  import { getSurface, getTarget, markTargetLifecycle, setTargetAvailability, updateTarget, } from '../runtime-state.js';
6
6
  import { incrementMetric, recordActionResult } from '../runtime-metrics.js';
7
7
  import { bumpPageScopeEpoch, registerPage, setCurrentPage } from '../runtime-page-state.js';
8
- import { clearProtectedExposure, getProtectedExposure } from '../runtime-protected-state.js';
8
+ import { clearProtectedExposure, getProtectedExposure, markFillableFormsAbsentForPage, } from '../runtime-protected-state.js';
9
9
  import { outputContractFailure, outputFailure, outputJSON, } from '../output.js';
10
10
  import { captureDiagnosticSnapshotBestEffort, finishDiagnosticStepBestEffort, recordDiagnosticArtifactManifestBestEffort, recordCommandLifecycleEventBestEffort, startDiagnosticStep, } from '../diagnostics.js';
11
11
  import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, genericClickObservationChanged, locatorStateChanged, pageObservationChanged, shouldVerifyObservableProgress, waitForAcceptanceProbe, } from './action-acceptance.js';
@@ -21,8 +21,10 @@ import { isLocatorUserActionable } from './user-actionable.js';
21
21
  import { resolveSurfaceScopeRoot } from './target-resolution.js';
22
22
  import { buildProtectedArtifactsSuppressed } from '../secrets/protected-artifact-guard.js';
23
23
  import { scrubProtectedExactValues } from '../secrets/protected-exact-value-redaction.js';
24
- import { connectPlaywright, disconnectPlaywright, listPages, resolvePageByRef, syncSessionPage, } from '../playwright-runtime.js';
24
+ import { listPages, resolvePageByRef, syncSessionPage } from '../playwright-runtime.js';
25
25
  import { withApiTraceContext } from '../command-api-tracing.js';
26
+ import { describeBrowserConnectionFailure } from './browser-connection-failure.js';
27
+ import { withStickyOwnerBrowser } from '../sticky-owner.js';
26
28
  function ensureValue(action, value) {
27
29
  if (action === 'click')
28
30
  return undefined;
@@ -504,7 +506,6 @@ export async function actBrowser(session, targetRef, action, value) {
504
506
  attempts.push(`action.alias:${requestedAction}->${action}`);
505
507
  }
506
508
  const startedAt = Date.now();
507
- let browser = null;
508
509
  let failureMessage = null;
509
510
  let failureArtifacts;
510
511
  let currentPage = null;
@@ -525,572 +526,583 @@ export async function actBrowser(session, targetRef, action, value) {
525
526
  finishFailure: async (_artifactDir) => undefined,
526
527
  };
527
528
  try {
528
- browser = await connectPlaywright(session.cdpUrl);
529
- }
530
- catch (err) {
531
- return buildActPreflightFailureResult({
532
- session,
533
- step: actStep,
534
- error: 'browser_connection_failed',
535
- outcomeType: 'blocked',
536
- message: 'The action could not start because AgentBrowse failed to connect to the browser.',
537
- reason: err instanceof Error ? err.message : String(err),
538
- targetRef,
539
- action,
540
- });
541
- }
542
- try {
543
- const page = await resolvePageByRef(browser, session, target.pageRef);
544
- currentPage = page;
545
- setCurrentPage(session, target.pageRef);
546
- const { url } = await syncSessionPage(session, target.pageRef, page);
547
- trace = await startActionTrace(page, {
548
- suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
549
- });
550
- if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
551
- staleReason = 'page-signature-mismatch';
552
- throw new Error('stale_target_page_signature_changed');
553
- }
554
- const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
555
- if (!liveTarget.domSignature) {
556
- return false;
557
- }
558
- for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
559
- const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
560
- if (attemptIndex > 0) {
561
- attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
562
- await new Promise((resolve) => setTimeout(resolve, delayMs));
563
- }
564
- const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
565
- if (!snapshot?.domSignature) {
566
- continue;
529
+ return await withStickyOwnerBrowser(session, async (browser) => {
530
+ try {
531
+ const page = await resolvePageByRef(browser, session, target.pageRef);
532
+ currentPage = page;
533
+ setCurrentPage(session, target.pageRef);
534
+ const { url } = await syncSessionPage(session, target.pageRef, page);
535
+ trace = await startActionTrace(page, {
536
+ suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
537
+ });
538
+ if (liveTarget.pageSignature &&
539
+ normalizePageSignature(url) !== liveTarget.pageSignature) {
540
+ staleReason = 'page-signature-mismatch';
541
+ throw new Error('stale_target_page_signature_changed');
567
542
  }
568
- if (snapshot.domSignature === liveTarget.domSignature) {
543
+ const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
544
+ if (!liveTarget.domSignature) {
545
+ return false;
546
+ }
547
+ for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
548
+ const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
549
+ if (attemptIndex > 0) {
550
+ attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
551
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
552
+ }
553
+ const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
554
+ if (!snapshot?.domSignature) {
555
+ continue;
556
+ }
557
+ if (snapshot.domSignature === liveTarget.domSignature) {
558
+ return false;
559
+ }
560
+ if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
561
+ continue;
562
+ }
563
+ attempts.push(`domSignature.rebound:${strategy}`);
564
+ const updatedTarget = updateTarget(session, targetRef, {
565
+ domSignature: snapshot.domSignature,
566
+ label: snapshot.label ?? liveTarget.label,
567
+ lifecycle: 'live',
568
+ lifecycleReason: undefined,
569
+ availability: { state: 'available' },
570
+ semantics: {
571
+ ...liveTarget.semantics,
572
+ name: snapshot.label ?? liveTarget.semantics?.name,
573
+ role: snapshot.role ?? liveTarget.semantics?.role,
574
+ },
575
+ });
576
+ if (updatedTarget) {
577
+ liveTarget = updatedTarget;
578
+ }
579
+ return true;
580
+ }
569
581
  return false;
582
+ };
583
+ const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
584
+ await assertStoredBindingStillValid(page, resolvedLocator, liveTarget, stage, {
585
+ onReason: async (reason, staleStage) => {
586
+ switch (reason) {
587
+ case 'page_signature_mismatch':
588
+ attempts.push(`stale.page-signature:${staleStage}`);
589
+ staleReason = 'page-signature-mismatch';
590
+ return false;
591
+ case 'locator_resolution_failed':
592
+ attempts.push(`stale.locator:${staleStage}`);
593
+ staleReason = 'locator-resolution-failed';
594
+ return false;
595
+ case 'dom_signature_mismatch': {
596
+ const rebound = await tryRebindMutableFieldTarget(resolvedLocator, staleStage);
597
+ if (rebound) {
598
+ return true;
599
+ }
600
+ attempts.push(`stale.dom-signature:${staleStage}`);
601
+ staleReason = 'dom-signature-mismatch';
602
+ return false;
603
+ }
604
+ }
605
+ },
606
+ errorForReason: (reason) => {
607
+ switch (reason) {
608
+ case 'page_signature_mismatch':
609
+ return 'stale_target_page_signature_changed';
610
+ case 'locator_resolution_failed':
611
+ return 'stale_target_locator_resolution_failed';
612
+ case 'dom_signature_mismatch':
613
+ return 'stale_target_dom_signature_changed';
614
+ }
615
+ },
616
+ });
617
+ };
618
+ let resolvedBy = null;
619
+ const beforePages = listPages(browser);
620
+ const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
621
+ const beforePageObservation = shouldCheckProgress
622
+ ? await capturePageObservation(page)
623
+ : null;
624
+ const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
625
+ let baseRoot = page;
626
+ let locatorRoot = baseRoot;
627
+ let surfaceRoot = null;
628
+ if (!deferSurfaceResolution) {
629
+ ({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
630
+ recordSelfTargetReuse: true,
631
+ }));
570
632
  }
571
- if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
572
- continue;
573
- }
574
- attempts.push(`domSignature.rebound:${strategy}`);
575
- const updatedTarget = updateTarget(session, targetRef, {
576
- domSignature: snapshot.domSignature,
577
- label: snapshot.label ?? liveTarget.label,
578
- lifecycle: 'live',
579
- lifecycleReason: undefined,
580
- availability: { state: 'available' },
581
- semantics: {
582
- ...liveTarget.semantics,
583
- name: snapshot.label ?? liveTarget.semantics?.name,
584
- role: snapshot.role ?? liveTarget.semantics?.role,
585
- },
586
- });
587
- if (updatedTarget) {
588
- liveTarget = updatedTarget;
589
- }
590
- return true;
591
- }
592
- return false;
593
- };
594
- const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
595
- await assertStoredBindingStillValid(page, resolvedLocator, liveTarget, stage, {
596
- onReason: async (reason, staleStage) => {
597
- switch (reason) {
598
- case 'page_signature_mismatch':
599
- attempts.push(`stale.page-signature:${staleStage}`);
600
- staleReason = 'page-signature-mismatch';
633
+ let lastError = null;
634
+ let sawDomSignatureMismatch = false;
635
+ let sawDisabledTarget = false;
636
+ let sawReadonlyTarget = false;
637
+ const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
638
+ if (!options?.skipDomSignature && liveTarget.domSignature) {
639
+ const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
640
+ if (!rebound) {
641
+ const liveSignature = await readLocatorDomSignature(resolvedLocator);
642
+ if (liveSignature && liveSignature !== liveTarget.domSignature) {
643
+ attempts.push(`domSignature.mismatch:${strategy}`);
644
+ sawDomSignatureMismatch = true;
645
+ return false;
646
+ }
647
+ }
648
+ }
649
+ let acceptanceProbe = null;
650
+ const tryRecoverActionErrorAcceptance = async () => {
651
+ if (!acceptanceProbe) {
601
652
  return false;
602
- case 'locator_resolution_failed':
603
- attempts.push(`stale.locator:${staleStage}`);
604
- staleReason = 'locator-resolution-failed';
653
+ }
654
+ const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
655
+ if (acceptance?.polls && acceptance.polls > 1) {
656
+ attempts.push(`acceptance.polled:${acceptance.polls}`);
657
+ }
658
+ if (!acceptance?.accepted) {
605
659
  return false;
606
- case 'dom_signature_mismatch': {
607
- const rebound = await tryRebindMutableFieldTarget(resolvedLocator, staleStage);
608
- if (rebound) {
660
+ }
661
+ if (acceptanceProbe.policy === 'submit') {
662
+ const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
663
+ if (!submitResolution.acceptAsProgress) {
664
+ return false;
665
+ }
666
+ attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
667
+ if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
668
+ attempts.push('submit-resolution:soft-result-candidate');
669
+ }
670
+ }
671
+ attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
672
+ incrementMetric(session, 'fallbackActions');
673
+ resolvedBy = 'playwright-locator';
674
+ locatorStrategy = strategy;
675
+ recoveredProgressProbe = acceptanceProbe;
676
+ progressProbe = null;
677
+ lastError = null;
678
+ recoveredAfterError = true;
679
+ recoveredAcceptancePolicy = acceptanceProbe.policy;
680
+ setTargetAvailability(session, targetRef, 'available');
681
+ return true;
682
+ };
683
+ try {
684
+ const valueProjection = await projectActionValue({
685
+ target,
686
+ action,
687
+ actionValue,
688
+ locator: resolvedLocator,
689
+ attempts,
690
+ });
691
+ const executionValue = valueProjection?.executionValue ?? actionValue;
692
+ const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
693
+ acceptanceProbe = await createAcceptanceProbe({
694
+ session,
695
+ page,
696
+ target,
697
+ action,
698
+ actionValue: acceptanceValue,
699
+ locator: resolvedLocator,
700
+ beforePageObservation,
701
+ });
702
+ attempts.push(`resolve:${strategy}`);
703
+ const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
704
+ clickActivationStrategy: clickActivationStrategyForTarget(target, action),
705
+ guards: {
706
+ assertStillValid: async (stage) => {
707
+ await assertResolvedTargetStillValid(resolvedLocator, stage);
708
+ },
709
+ },
710
+ });
711
+ if (usedFallback) {
712
+ incrementMetric(session, 'fallbackActions');
713
+ }
714
+ resolvedBy = 'playwright-locator';
715
+ locatorStrategy = strategy;
716
+ progressProbe = acceptanceProbe;
717
+ setTargetAvailability(session, targetRef, 'available');
718
+ return true;
719
+ }
720
+ catch (err) {
721
+ lastError = err instanceof Error ? err : new Error(String(err));
722
+ const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
723
+ (acceptanceProbe.policy === 'value-change' ||
724
+ acceptanceProbe.policy === 'selection' ||
725
+ acceptanceProbe.policy === 'date-selection' ||
726
+ acceptanceProbe.policy === 'navigation' ||
727
+ acceptanceProbe.policy === 'submit');
728
+ if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
729
+ return true;
730
+ }
731
+ if (staleReason) {
732
+ throw lastError;
733
+ }
734
+ try {
735
+ await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
736
+ }
737
+ catch (validationError) {
738
+ if ((acceptanceProbe?.policy === 'navigation' ||
739
+ acceptanceProbe?.policy === 'submit') &&
740
+ validationError instanceof Error &&
741
+ validationError.message === 'stale_target_page_signature_changed' &&
742
+ (await tryRecoverActionErrorAcceptance())) {
609
743
  return true;
610
744
  }
611
- attempts.push(`stale.dom-signature:${staleStage}`);
612
- staleReason = 'dom-signature-mismatch';
613
- return false;
745
+ throw validationError;
614
746
  }
747
+ return false;
615
748
  }
616
- },
617
- errorForReason: (reason) => {
618
- switch (reason) {
619
- case 'page_signature_mismatch':
620
- return 'stale_target_page_signature_changed';
621
- case 'locator_resolution_failed':
622
- return 'stale_target_locator_resolution_failed';
623
- case 'dom_signature_mismatch':
624
- return 'stale_target_dom_signature_changed';
749
+ };
750
+ const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
751
+ const popupPromise = watchForNewPage
752
+ ? waitForPopup(page.context())
753
+ : Promise.resolve(null);
754
+ const tryRankedCandidates = async () => {
755
+ const resolution = await resolvePreparedLocatorCandidates({
756
+ target,
757
+ action,
758
+ baseRoot,
759
+ locatorRoot,
760
+ surfaceRoot,
761
+ attempts,
762
+ prepareOptions: {
763
+ allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
764
+ allowDescendantPressFallback: action === 'press' &&
765
+ (target.controlFamily === 'text-input' ||
766
+ target.controlFamily === 'select' ||
767
+ target.controlFamily === 'datepicker'),
768
+ isUserActionable: isLocatorUserActionable,
769
+ },
770
+ onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy),
771
+ });
772
+ if (resolution.sawDisabledTarget) {
773
+ sawDisabledTarget = true;
625
774
  }
626
- },
627
- });
628
- };
629
- let resolvedBy = null;
630
- const beforePages = listPages(browser);
631
- const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
632
- const beforePageObservation = shouldCheckProgress
633
- ? await capturePageObservation(page)
634
- : null;
635
- const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
636
- let baseRoot = page;
637
- let locatorRoot = baseRoot;
638
- let surfaceRoot = null;
639
- if (!deferSurfaceResolution) {
640
- ({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
641
- recordSelfTargetReuse: true,
642
- }));
643
- }
644
- let lastError = null;
645
- let sawDomSignatureMismatch = false;
646
- let sawDisabledTarget = false;
647
- let sawReadonlyTarget = false;
648
- const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
649
- if (!options?.skipDomSignature && liveTarget.domSignature) {
650
- const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
651
- if (!rebound) {
652
- const liveSignature = await readLocatorDomSignature(resolvedLocator);
653
- if (liveSignature && liveSignature !== liveTarget.domSignature) {
654
- attempts.push(`domSignature.mismatch:${strategy}`);
655
- sawDomSignatureMismatch = true;
656
- return false;
775
+ if (resolution.sawReadonlyTarget) {
776
+ sawReadonlyTarget = true;
777
+ }
778
+ };
779
+ await tryRankedCandidates();
780
+ if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
781
+ const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
782
+ if (deferredSurfaceRoot) {
783
+ surfaceRoot = deferredSurfaceRoot;
784
+ locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface)
785
+ ? baseRoot
786
+ : surfaceRoot;
787
+ await tryRankedCandidates();
657
788
  }
658
789
  }
659
- }
660
- let acceptanceProbe = null;
661
- const tryRecoverActionErrorAcceptance = async () => {
662
- if (!acceptanceProbe) {
663
- return false;
790
+ if (!resolvedBy && !lastError && surfaceRoot) {
791
+ const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
792
+ if (recoveredLocator) {
793
+ await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
794
+ skipDomSignature: true,
795
+ });
796
+ }
664
797
  }
665
- const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
666
- if (acceptance?.polls && acceptance.polls > 1) {
667
- attempts.push(`acceptance.polled:${acceptance.polls}`);
798
+ if (!resolvedBy) {
799
+ if (sawDomSignatureMismatch) {
800
+ staleReason = 'dom-signature-mismatch';
801
+ throw new Error('stale_target_dom_signature_changed');
802
+ }
803
+ if (sawDisabledTarget) {
804
+ setTargetAvailability(session, targetRef, 'gated', 'disabled');
805
+ throw new Error('target_disabled');
806
+ }
807
+ if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
808
+ setTargetAvailability(session, targetRef, 'gated', 'readonly');
809
+ throw new Error('target_readonly');
810
+ }
811
+ if (!lastError &&
812
+ target.surfaceRef &&
813
+ (target.acceptancePolicy === 'selection' ||
814
+ target.acceptancePolicy === 'date-selection')) {
815
+ setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
816
+ throw new Error('target_surface_inactive');
817
+ }
818
+ if (!resolvedBy) {
819
+ if (!lastError &&
820
+ (target.controlFamily === 'text-input' ||
821
+ target.controlFamily === 'select' ||
822
+ target.controlFamily === 'datepicker')) {
823
+ staleReason = 'locator-resolution-failed';
824
+ throw new Error('stale_target_locator_resolution_failed');
825
+ }
826
+ if (lastError) {
827
+ throw lastError;
828
+ }
829
+ throw new Error('deterministic_target_resolution_failed');
830
+ }
668
831
  }
669
- if (!acceptance?.accepted) {
670
- return false;
832
+ const popup = await popupPromise;
833
+ const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
834
+ if (latePage) {
835
+ attempts.push('late-page-captured');
671
836
  }
672
- if (acceptanceProbe.policy === 'submit') {
673
- const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
674
- if (!submitResolution.acceptAsProgress) {
675
- return false;
837
+ const discoveredPage = popup ?? latePage;
838
+ const afterPages = discoveredPage
839
+ ? [...beforePages, discoveredPage]
840
+ : listPages(browser);
841
+ const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
842
+ let finalPageRef = target.pageRef;
843
+ if (capturedPopup) {
844
+ await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
845
+ settleTimeoutMs: 1_500,
846
+ });
847
+ setCurrentPage(session, capturedPopup.page.pageRef);
848
+ finalPageRef = capturedPopup.page.pageRef;
849
+ currentPageRef = finalPageRef;
850
+ }
851
+ else {
852
+ const syncedPage = await syncSessionPage(session, target.pageRef, page);
853
+ currentPageRef = target.pageRef;
854
+ if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
855
+ clearProtectedExposure(session, target.pageRef);
856
+ markFillableFormsAbsentForPage(session, target.pageRef);
676
857
  }
677
- attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
678
- if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
679
- attempts.push('submit-resolution:soft-result-candidate');
858
+ const progressProbeForVerification = progressProbe;
859
+ if (progressProbeForVerification) {
860
+ const finalProgressProbe = progressProbeForVerification;
861
+ const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
862
+ const afterPageObservation = acceptance.afterPageObservation;
863
+ const accepted = acceptance.accepted;
864
+ const submitResolution = finalProgressProbe.policy === 'submit'
865
+ ? await resolveSubmitResult(finalProgressProbe, afterPageObservation)
866
+ : null;
867
+ if (acceptance.polls > 1) {
868
+ attempts.push(`acceptance.polled:${acceptance.polls}`);
869
+ }
870
+ if (submitResolution?.finalVerdict === 'blocked') {
871
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
872
+ noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
873
+ attempts.push('submit-resolution:blocked');
874
+ throw new Error('validation_blocked');
875
+ }
876
+ if (!accepted) {
877
+ if (finalProgressProbe.policy === 'value-change') {
878
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
879
+ throw new Error('action_postcondition_failed:value-change');
880
+ }
881
+ if ((finalProgressProbe.policy === 'selection' ||
882
+ finalProgressProbe.policy === 'date-selection') &&
883
+ finalProgressProbe.expectedValue !== null) {
884
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
885
+ throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
886
+ }
887
+ if (finalProgressProbe.policy === 'submit' && submitResolution) {
888
+ if (submitResolution.acceptAsProgress) {
889
+ attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
890
+ if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
891
+ attempts.push('submit-resolution:soft-result-candidate');
892
+ }
893
+ }
894
+ else {
895
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
896
+ noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
897
+ if (hasValidationBlockedObservations(noProgressObservations)) {
898
+ attempts.push('submit-resolution:blocked');
899
+ throw new Error('validation_blocked');
900
+ }
901
+ attempts.push('no-progress.detected');
902
+ throw new Error('no_observable_progress');
903
+ }
904
+ }
905
+ else {
906
+ const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
907
+ ? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
908
+ : null;
909
+ const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
910
+ const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
911
+ Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
912
+ Boolean(finalProgressProbe.beforePage || afterPageObservation);
913
+ const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
914
+ ? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
915
+ : pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
916
+ if (hasComparableSignal &&
917
+ !pageProgressChanged &&
918
+ finalProgressProbe.beforeContextHash === afterContextHash &&
919
+ !locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
920
+ attempts.push('no-progress.detected');
921
+ noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
922
+ throw new Error('no_observable_progress');
923
+ }
924
+ }
925
+ }
926
+ else {
927
+ partialProgressResult = await partialProgressForAliasedSelection({
928
+ requestedAction,
929
+ probe: finalProgressProbe,
930
+ });
931
+ }
932
+ }
933
+ else if (recoveredProgressProbe) {
934
+ partialProgressResult = await partialProgressForAliasedSelection({
935
+ requestedAction,
936
+ probe: recoveredProgressProbe,
937
+ });
680
938
  }
681
939
  }
682
- attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
683
- incrementMetric(session, 'fallbackActions');
684
- resolvedBy = 'playwright-locator';
685
- locatorStrategy = strategy;
686
- recoveredProgressProbe = acceptanceProbe;
687
- progressProbe = null;
688
- lastError = null;
689
- recoveredAfterError = true;
690
- recoveredAcceptancePolicy = acceptanceProbe.policy;
691
- setTargetAvailability(session, targetRef, 'available');
692
- return true;
693
- };
694
- try {
695
- const valueProjection = await projectActionValue({
696
- target,
697
- action,
698
- actionValue,
699
- locator: resolvedLocator,
700
- attempts,
701
- });
702
- const executionValue = valueProjection?.executionValue ?? actionValue;
703
- const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
704
- acceptanceProbe = await createAcceptanceProbe({
940
+ if (resolvedBy === 'playwright-locator') {
941
+ incrementMetric(session, 'deterministicActions');
942
+ }
943
+ bumpPageScopeEpoch(session, target.pageRef);
944
+ recordActionResult(session, true, Date.now() - startedAt);
945
+ await trace.finishSuccess();
946
+ captureDiagnosticSnapshotBestEffort({
705
947
  session,
706
- page,
707
- target,
708
- action,
709
- actionValue: acceptanceValue,
710
- locator: resolvedLocator,
711
- beforePageObservation,
948
+ step: actStep,
949
+ phase: 'after',
950
+ pageRef: finalPageRef,
712
951
  });
713
- attempts.push(`resolve:${strategy}`);
714
- const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
715
- clickActivationStrategy: clickActivationStrategyForTarget(target, action),
716
- guards: {
717
- assertStillValid: async (stage) => {
718
- await assertResolvedTargetStillValid(resolvedLocator, stage);
719
- },
952
+ recordCommandLifecycleEventBestEffort({
953
+ step: actStep,
954
+ phase: 'completed',
955
+ attributes: {
956
+ outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
957
+ targetRef,
958
+ action: requestedAction,
959
+ pageRef: finalPageRef,
720
960
  },
721
961
  });
722
- if (usedFallback) {
723
- incrementMetric(session, 'fallbackActions');
724
- }
725
- resolvedBy = 'playwright-locator';
726
- locatorStrategy = strategy;
727
- progressProbe = acceptanceProbe;
728
- setTargetAvailability(session, targetRef, 'available');
729
- return true;
962
+ await finalizeActStepBestEffort(actStep, {
963
+ success: true,
964
+ outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
965
+ message: partialProgressResult?.message ?? 'The requested action completed successfully.',
966
+ });
967
+ return scrubProtectedExactValues(session, {
968
+ success: true,
969
+ targetRef,
970
+ action: requestedAction,
971
+ ...(action !== requestedAction ? { executedAs: action } : {}),
972
+ value: actionValue,
973
+ resolvedBy,
974
+ locatorStrategy,
975
+ pageRef: finalPageRef,
976
+ attempts: sanitizePublicAttempts(attempts),
977
+ popup: Boolean(capturedPopup),
978
+ overlayHandled: attempts.includes('overlay.dismissed'),
979
+ iframe: Boolean(target.framePath?.length),
980
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
981
+ ...(recoveredAfterError
982
+ ? {
983
+ recoveredAfterError: true,
984
+ recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
985
+ }
986
+ : {}),
987
+ ...(partialProgressResult ?? {}),
988
+ durationMs: Date.now() - startedAt,
989
+ metrics: session.runtime?.metrics,
990
+ });
730
991
  }
731
992
  catch (err) {
732
- lastError = err instanceof Error ? err : new Error(String(err));
733
- const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
734
- (acceptanceProbe.policy === 'value-change' ||
735
- acceptanceProbe.policy === 'selection' ||
736
- acceptanceProbe.policy === 'date-selection' ||
737
- acceptanceProbe.policy === 'navigation' ||
738
- acceptanceProbe.policy === 'submit');
739
- if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
740
- return true;
741
- }
993
+ failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
994
+ recordActionResult(session, false, Date.now() - startedAt);
742
995
  if (staleReason) {
743
- throw lastError;
744
- }
745
- try {
746
- await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
747
- }
748
- catch (validationError) {
749
- if ((acceptanceProbe?.policy === 'navigation' ||
750
- acceptanceProbe?.policy === 'submit') &&
751
- validationError instanceof Error &&
752
- validationError.message === 'stale_target_page_signature_changed' &&
753
- (await tryRecoverActionErrorAcceptance())) {
754
- return true;
755
- }
756
- throw validationError;
757
- }
758
- return false;
759
- }
760
- };
761
- const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
762
- const popupPromise = watchForNewPage
763
- ? waitForPopup(page.context())
764
- : Promise.resolve(null);
765
- const tryRankedCandidates = async () => {
766
- const resolution = await resolvePreparedLocatorCandidates({
767
- target,
768
- action,
769
- baseRoot,
770
- locatorRoot,
771
- surfaceRoot,
772
- attempts,
773
- prepareOptions: {
774
- allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
775
- allowDescendantPressFallback: action === 'press' &&
776
- (target.controlFamily === 'text-input' ||
777
- target.controlFamily === 'select' ||
778
- target.controlFamily === 'datepicker'),
779
- isUserActionable: isLocatorUserActionable,
780
- },
781
- onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy),
782
- });
783
- if (resolution.sawDisabledTarget) {
784
- sawDisabledTarget = true;
785
- }
786
- if (resolution.sawReadonlyTarget) {
787
- sawReadonlyTarget = true;
788
- }
789
- };
790
- await tryRankedCandidates();
791
- if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
792
- const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
793
- if (deferredSurfaceRoot) {
794
- surfaceRoot = deferredSurfaceRoot;
795
- locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface)
796
- ? baseRoot
797
- : surfaceRoot;
798
- await tryRankedCandidates();
799
- }
800
- }
801
- if (!resolvedBy && !lastError && surfaceRoot) {
802
- const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
803
- if (recoveredLocator) {
804
- await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
805
- skipDomSignature: true,
806
- });
807
- }
808
- }
809
- if (!resolvedBy) {
810
- if (sawDomSignatureMismatch) {
811
- staleReason = 'dom-signature-mismatch';
812
- throw new Error('stale_target_dom_signature_changed');
813
- }
814
- if (sawDisabledTarget) {
815
- setTargetAvailability(session, targetRef, 'gated', 'disabled');
816
- throw new Error('target_disabled');
817
- }
818
- if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
819
- setTargetAvailability(session, targetRef, 'gated', 'readonly');
820
- throw new Error('target_readonly');
821
- }
822
- if (!lastError &&
823
- target.surfaceRef &&
824
- (target.acceptancePolicy === 'selection' ||
825
- target.acceptancePolicy === 'date-selection')) {
826
- setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
827
- throw new Error('target_surface_inactive');
828
- }
829
- if (!resolvedBy) {
830
- if (!lastError &&
831
- (target.controlFamily === 'text-input' ||
832
- target.controlFamily === 'select' ||
833
- target.controlFamily === 'datepicker')) {
834
- staleReason = 'locator-resolution-failed';
835
- throw new Error('stale_target_locator_resolution_failed');
836
- }
837
- if (lastError) {
838
- throw lastError;
996
+ markTargetLifecycle(session, targetRef, 'stale', staleReason);
839
997
  }
840
- throw new Error('deterministic_target_resolution_failed');
841
- }
842
- }
843
- const popup = await popupPromise;
844
- const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
845
- if (latePage) {
846
- attempts.push('late-page-captured');
847
- }
848
- const discoveredPage = popup ?? latePage;
849
- const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
850
- const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
851
- let finalPageRef = target.pageRef;
852
- if (capturedPopup) {
853
- await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
854
- settleTimeoutMs: 1_500,
855
- });
856
- setCurrentPage(session, capturedPopup.page.pageRef);
857
- finalPageRef = capturedPopup.page.pageRef;
858
- currentPageRef = finalPageRef;
859
- }
860
- else {
861
- const syncedPage = await syncSessionPage(session, target.pageRef, page);
862
- currentPageRef = target.pageRef;
863
- if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
864
- clearProtectedExposure(session, target.pageRef);
865
- }
866
- const progressProbeForVerification = progressProbe;
867
- if (progressProbeForVerification) {
868
- const finalProgressProbe = progressProbeForVerification;
869
- const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
870
- const afterPageObservation = acceptance.afterPageObservation;
871
- const accepted = acceptance.accepted;
872
- if (acceptance.polls > 1) {
873
- attempts.push(`acceptance.polled:${acceptance.polls}`);
874
- }
875
- if (!accepted) {
876
- if (finalProgressProbe.policy === 'value-change') {
877
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
878
- throw new Error('action_postcondition_failed:value-change');
879
- }
880
- if ((finalProgressProbe.policy === 'selection' ||
881
- finalProgressProbe.policy === 'date-selection') &&
882
- finalProgressProbe.expectedValue !== null) {
883
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
884
- throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
885
- }
886
- if (finalProgressProbe.policy === 'submit') {
887
- const submitResolution = await resolveSubmitResult(finalProgressProbe, afterPageObservation);
888
- if (submitResolution.acceptAsProgress) {
889
- attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
890
- if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
891
- attempts.push('submit-resolution:soft-result-candidate');
892
- }
998
+ if (currentPage) {
999
+ try {
1000
+ const protectedExposure = getProtectedExposure(session, currentPageRef);
1001
+ if (protectedExposure) {
1002
+ await trace.finishSuccess();
1003
+ failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
893
1004
  }
894
1005
  else {
895
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
896
- noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
897
- if (submitResolution.finalVerdict === 'blocked' ||
898
- hasValidationBlockedObservations(noProgressObservations)) {
899
- attempts.push('submit-resolution:blocked');
900
- throw new Error('validation_blocked');
901
- }
902
- attempts.push('no-progress.detected');
903
- throw new Error('no_observable_progress');
1006
+ failureArtifacts = await captureActionFailureArtifacts({
1007
+ page: currentPage,
1008
+ targetRef,
1009
+ action: requestedAction,
1010
+ pageRef: currentPageRef,
1011
+ attempts,
1012
+ locatorStrategy,
1013
+ popup: attempts.includes('popup-captured'),
1014
+ overlayHandled: attempts.includes('overlay.dismissed'),
1015
+ iframe: Boolean(target.framePath?.length),
1016
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1017
+ durationMs: Date.now() - startedAt,
1018
+ error: failureMessage,
1019
+ finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
1020
+ });
904
1021
  }
905
1022
  }
906
- else {
907
- const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
908
- ? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
909
- : null;
910
- const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
911
- const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
912
- Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
913
- Boolean(finalProgressProbe.beforePage || afterPageObservation);
914
- const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
915
- ? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
916
- : pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
917
- if (hasComparableSignal &&
918
- !pageProgressChanged &&
919
- finalProgressProbe.beforeContextHash === afterContextHash &&
920
- !locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
921
- attempts.push('no-progress.detected');
922
- noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
923
- throw new Error('no_observable_progress');
924
- }
1023
+ catch {
1024
+ // Best effort only. Preserve the original action failure.
925
1025
  }
926
1026
  }
927
- else {
928
- partialProgressResult = await partialProgressForAliasedSelection({
929
- requestedAction,
930
- probe: finalProgressProbe,
931
- });
932
- }
933
1027
  }
934
- else if (recoveredProgressProbe) {
935
- partialProgressResult = await partialProgressForAliasedSelection({
936
- requestedAction,
937
- probe: recoveredProgressProbe,
938
- });
1028
+ if (!failureMessage) {
1029
+ throw new Error('unreachable_action_completion_state');
939
1030
  }
940
- }
941
- if (resolvedBy === 'playwright-locator') {
942
- incrementMetric(session, 'deterministicActions');
943
- }
944
- bumpPageScopeEpoch(session, target.pageRef);
945
- recordActionResult(session, true, Date.now() - startedAt);
946
- await trace.finishSuccess();
947
- captureDiagnosticSnapshotBestEffort({
948
- session,
949
- step: actStep,
950
- phase: 'after',
951
- pageRef: finalPageRef,
952
- });
953
- recordCommandLifecycleEventBestEffort({
954
- step: actStep,
955
- phase: 'completed',
956
- attributes: {
957
- outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
1031
+ const failureContract = describeActFailure({
1032
+ failureMessage,
1033
+ staleReason,
1034
+ });
1035
+ const outputObservations = hasMeaningfulNoObservableProgressObservations(noProgressObservations)
1036
+ ? noProgressObservations
1037
+ : undefined;
1038
+ const artifactManifestId = await persistActArtifactManifestBestEffort(runId, actStep, actStep?.stepId, failureArtifacts ?? null);
1039
+ captureDiagnosticSnapshotBestEffort({
1040
+ session,
1041
+ step: actStep,
1042
+ phase: 'point-in-time',
1043
+ pageRef: currentPageRef,
1044
+ artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
1045
+ });
1046
+ recordCommandLifecycleEventBestEffort({
1047
+ step: actStep,
1048
+ phase: 'failed',
1049
+ attributes: {
1050
+ outcomeType: failureContract.outcomeType,
1051
+ targetRef,
1052
+ action: requestedAction,
1053
+ pageRef: currentPageRef,
1054
+ ...(artifactManifestId ? { artifactManifestId } : {}),
1055
+ },
1056
+ });
1057
+ await finalizeActStepBestEffort(actStep, {
1058
+ success: false,
1059
+ outcomeType: failureContract.outcomeType,
1060
+ message: failureContract.message,
1061
+ reason: failureContract.reason,
1062
+ artifactManifestId,
1063
+ });
1064
+ return scrubProtectedExactValues(session, {
1065
+ success: false,
1066
+ failureSurface: 'output',
1067
+ error: failureContract.error,
1068
+ outcomeType: failureContract.outcomeType,
1069
+ message: failureContract.message,
1070
+ reason: failureContract.reason,
958
1071
  targetRef,
959
1072
  action: requestedAction,
960
- pageRef: finalPageRef,
961
- },
1073
+ ...(action !== requestedAction ? { executedAs: action } : {}),
1074
+ value: actionValue,
1075
+ pageRef: currentPageRef,
1076
+ locatorStrategy,
1077
+ attempts: sanitizePublicAttempts(attempts),
1078
+ popup: attempts.includes('popup-captured'),
1079
+ overlayHandled: attempts.includes('overlay.dismissed'),
1080
+ iframe: Boolean(target.framePath?.length),
1081
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1082
+ durationMs: Date.now() - startedAt,
1083
+ staleTarget: Boolean(staleReason),
1084
+ observations: outputObservations,
1085
+ artifacts: failureArtifacts,
1086
+ metrics: session.runtime?.metrics,
1087
+ });
962
1088
  });
963
- await finalizeActStepBestEffort(actStep, {
964
- success: true,
965
- outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
966
- message: partialProgressResult?.message ?? 'The requested action completed successfully.',
1089
+ }
1090
+ catch (err) {
1091
+ const browserConnectionFailure = describeBrowserConnectionFailure(err, {
1092
+ defaultMessage: 'The action could not start because AgentBrowse failed to connect to the browser.',
1093
+ unrecoverableSessionMessage: 'The action could not start because the previous browser session is no longer reachable.',
967
1094
  });
968
- return scrubProtectedExactValues(session, {
969
- success: true,
1095
+ return buildActPreflightFailureResult({
1096
+ session,
1097
+ step: actStep,
1098
+ error: 'browser_connection_failed',
1099
+ outcomeType: 'blocked',
1100
+ message: browserConnectionFailure.message,
1101
+ reason: browserConnectionFailure.reason,
970
1102
  targetRef,
971
- action: requestedAction,
972
- ...(action !== requestedAction ? { executedAs: action } : {}),
973
- value: actionValue,
974
- resolvedBy,
975
- locatorStrategy,
976
- pageRef: finalPageRef,
977
- attempts: sanitizePublicAttempts(attempts),
978
- popup: Boolean(capturedPopup),
979
- overlayHandled: attempts.includes('overlay.dismissed'),
980
- iframe: Boolean(target.framePath?.length),
981
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
982
- ...(recoveredAfterError
983
- ? {
984
- recoveredAfterError: true,
985
- recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
986
- }
987
- : {}),
988
- ...(partialProgressResult ?? {}),
989
- durationMs: Date.now() - startedAt,
990
- metrics: session.runtime?.metrics,
1103
+ action,
991
1104
  });
992
1105
  }
993
- catch (err) {
994
- failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
995
- recordActionResult(session, false, Date.now() - startedAt);
996
- if (staleReason) {
997
- markTargetLifecycle(session, targetRef, 'stale', staleReason);
998
- }
999
- if (currentPage) {
1000
- try {
1001
- const protectedExposure = getProtectedExposure(session, currentPageRef);
1002
- if (protectedExposure) {
1003
- await trace.finishSuccess();
1004
- failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
1005
- }
1006
- else {
1007
- failureArtifacts = await captureActionFailureArtifacts({
1008
- page: currentPage,
1009
- targetRef,
1010
- action: requestedAction,
1011
- pageRef: currentPageRef,
1012
- attempts,
1013
- locatorStrategy,
1014
- popup: attempts.includes('popup-captured'),
1015
- overlayHandled: attempts.includes('overlay.dismissed'),
1016
- iframe: Boolean(target.framePath?.length),
1017
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1018
- durationMs: Date.now() - startedAt,
1019
- error: failureMessage,
1020
- finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
1021
- });
1022
- }
1023
- }
1024
- catch {
1025
- // Best effort only. Preserve the original action failure.
1026
- }
1027
- }
1028
- }
1029
- finally {
1030
- if (browser) {
1031
- await disconnectPlaywright(browser);
1032
- }
1033
- }
1034
- if (!failureMessage) {
1035
- throw new Error('unreachable_action_completion_state');
1036
- }
1037
- const failureContract = describeActFailure({
1038
- failureMessage,
1039
- staleReason,
1040
- });
1041
- const outputObservations = hasMeaningfulNoObservableProgressObservations(noProgressObservations)
1042
- ? noProgressObservations
1043
- : undefined;
1044
- const artifactManifestId = await persistActArtifactManifestBestEffort(runId, actStep, actStep?.stepId, failureArtifacts ?? null);
1045
- captureDiagnosticSnapshotBestEffort({
1046
- session,
1047
- step: actStep,
1048
- phase: 'point-in-time',
1049
- pageRef: currentPageRef,
1050
- artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
1051
- });
1052
- recordCommandLifecycleEventBestEffort({
1053
- step: actStep,
1054
- phase: 'failed',
1055
- attributes: {
1056
- outcomeType: failureContract.outcomeType,
1057
- targetRef,
1058
- action: requestedAction,
1059
- pageRef: currentPageRef,
1060
- ...(artifactManifestId ? { artifactManifestId } : {}),
1061
- },
1062
- });
1063
- await finalizeActStepBestEffort(actStep, {
1064
- success: false,
1065
- outcomeType: failureContract.outcomeType,
1066
- message: failureContract.message,
1067
- reason: failureContract.reason,
1068
- artifactManifestId,
1069
- });
1070
- return scrubProtectedExactValues(session, {
1071
- success: false,
1072
- failureSurface: 'output',
1073
- error: failureContract.error,
1074
- outcomeType: failureContract.outcomeType,
1075
- message: failureContract.message,
1076
- reason: failureContract.reason,
1077
- targetRef,
1078
- action: requestedAction,
1079
- ...(action !== requestedAction ? { executedAs: action } : {}),
1080
- value: actionValue,
1081
- pageRef: currentPageRef,
1082
- locatorStrategy,
1083
- attempts: sanitizePublicAttempts(attempts),
1084
- popup: attempts.includes('popup-captured'),
1085
- overlayHandled: attempts.includes('overlay.dismissed'),
1086
- iframe: Boolean(target.framePath?.length),
1087
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1088
- durationMs: Date.now() - startedAt,
1089
- staleTarget: Boolean(staleReason),
1090
- observations: outputObservations,
1091
- artifacts: failureArtifacts,
1092
- metrics: session.runtime?.metrics,
1093
- });
1094
1106
  });
1095
1107
  }
1096
1108
  export async function act(session, targetRef, action, value) {