@nuanu-ai/agentbrowse 0.2.46 → 0.2.48

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 (190) hide show
  1. package/README.md +69 -10
  2. package/dist/agentpay-gateway.d.ts +9 -0
  3. package/dist/agentpay-gateway.d.ts.map +1 -1
  4. package/dist/agentpay-gateway.js +30 -0
  5. package/dist/agentpay-stagehand-llm.d.ts.map +1 -1
  6. package/dist/agentpay-stagehand-llm.js +9 -1
  7. package/dist/command-api-tracing.d.ts +19 -0
  8. package/dist/command-api-tracing.d.ts.map +1 -0
  9. package/dist/command-api-tracing.js +137 -0
  10. package/dist/commands/act.d.ts.map +1 -1
  11. package/dist/commands/act.js +822 -670
  12. package/dist/commands/act.test-harness.d.ts +6 -0
  13. package/dist/commands/act.test-harness.d.ts.map +1 -1
  14. package/dist/commands/act.test-harness.js +44 -1
  15. package/dist/commands/action-acceptance.d.ts.map +1 -1
  16. package/dist/commands/action-acceptance.js +115 -0
  17. package/dist/commands/captcha-solve.d.ts.map +1 -1
  18. package/dist/commands/captcha-solve.js +83 -16
  19. package/dist/commands/click-action-executor.d.ts +0 -1
  20. package/dist/commands/click-action-executor.d.ts.map +1 -1
  21. package/dist/commands/click-action-executor.js +31 -77
  22. package/dist/commands/close.d.ts +3 -3
  23. package/dist/commands/close.d.ts.map +1 -1
  24. package/dist/commands/close.js +178 -0
  25. package/dist/commands/descriptor-validation.d.ts.map +1 -1
  26. package/dist/commands/descriptor-validation.js +75 -57
  27. package/dist/commands/end-session.d.ts +25 -0
  28. package/dist/commands/end-session.d.ts.map +1 -0
  29. package/dist/commands/end-session.js +161 -0
  30. package/dist/commands/extract-stagehand-executor.js +1 -1
  31. package/dist/commands/extract.d.ts.map +1 -1
  32. package/dist/commands/extract.js +339 -202
  33. package/dist/commands/fill-secret.d.ts +3 -3
  34. package/dist/commands/fill-secret.d.ts.map +1 -1
  35. package/dist/commands/fill-secret.js +419 -234
  36. package/dist/commands/get-secrets-catalog.d.ts.map +1 -1
  37. package/dist/commands/get-secrets-catalog.js +66 -5
  38. package/dist/commands/interaction-kernel.d.ts +46 -0
  39. package/dist/commands/interaction-kernel.d.ts.map +1 -0
  40. package/dist/commands/interaction-kernel.js +215 -0
  41. package/dist/commands/launch.d.ts +0 -2
  42. package/dist/commands/launch.d.ts.map +1 -1
  43. package/dist/commands/launch.js +109 -17
  44. package/dist/commands/navigate.d.ts.map +1 -1
  45. package/dist/commands/navigate.js +188 -45
  46. package/dist/commands/observe-accessibility.d.ts.map +1 -1
  47. package/dist/commands/observe-accessibility.js +50 -39
  48. package/dist/commands/observe-dom-label-contract.d.ts.map +1 -1
  49. package/dist/commands/observe-dom-label-contract.js +5 -0
  50. package/dist/commands/observe-inventory.d.ts +13 -0
  51. package/dist/commands/observe-inventory.d.ts.map +1 -1
  52. package/dist/commands/observe-inventory.js +320 -65
  53. package/dist/commands/observe-persistence.d.ts.map +1 -1
  54. package/dist/commands/observe-persistence.js +3 -0
  55. package/dist/commands/observe-projection.d.ts +1 -0
  56. package/dist/commands/observe-projection.d.ts.map +1 -1
  57. package/dist/commands/observe-projection.js +7 -2
  58. package/dist/commands/observe-protected.d.ts +1 -0
  59. package/dist/commands/observe-protected.d.ts.map +1 -1
  60. package/dist/commands/observe-protected.js +9 -4
  61. package/dist/commands/observe-semantics.d.ts.map +1 -1
  62. package/dist/commands/observe-semantics.js +5 -2
  63. package/dist/commands/observe-stagehand.d.ts +1 -0
  64. package/dist/commands/observe-stagehand.d.ts.map +1 -1
  65. package/dist/commands/observe-stagehand.js +2 -0
  66. package/dist/commands/observe.d.ts +2 -0
  67. package/dist/commands/observe.d.ts.map +1 -1
  68. package/dist/commands/observe.js +387 -203
  69. package/dist/commands/observe.test-harness.d.ts +8 -0
  70. package/dist/commands/observe.test-harness.d.ts.map +1 -1
  71. package/dist/commands/observe.test-harness.js +48 -1
  72. package/dist/commands/poll-secret.d.ts +6 -0
  73. package/dist/commands/poll-secret.d.ts.map +1 -0
  74. package/dist/commands/poll-secret.js +159 -0
  75. package/dist/commands/request-secret.d.ts +6 -0
  76. package/dist/commands/request-secret.d.ts.map +1 -0
  77. package/dist/commands/request-secret.js +284 -0
  78. package/dist/commands/screenshot.d.ts.map +1 -1
  79. package/dist/commands/screenshot.js +172 -7
  80. package/dist/commands/select-action-executor.d.ts.map +1 -1
  81. package/dist/commands/semantic-observe.d.ts +4 -0
  82. package/dist/commands/semantic-observe.d.ts.map +1 -1
  83. package/dist/commands/semantic-observe.js +388 -17
  84. package/dist/commands/start-session.d.ts +31 -0
  85. package/dist/commands/start-session.d.ts.map +1 -0
  86. package/dist/commands/start-session.js +347 -0
  87. package/dist/commands/status.d.ts +2 -1
  88. package/dist/commands/status.d.ts.map +1 -1
  89. package/dist/commands/status.js +166 -144
  90. package/dist/control-semantics.d.ts +1 -0
  91. package/dist/control-semantics.d.ts.map +1 -1
  92. package/dist/control-semantics.js +51 -9
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +144 -45
  95. package/dist/otel-exporter.d.ts +58 -0
  96. package/dist/otel-exporter.d.ts.map +1 -0
  97. package/dist/otel-exporter.js +263 -0
  98. package/dist/otel-projector.d.ts +75 -0
  99. package/dist/otel-projector.d.ts.map +1 -0
  100. package/dist/otel-projector.js +409 -0
  101. package/dist/owned-browser.d.ts +1 -1
  102. package/dist/owned-browser.d.ts.map +1 -1
  103. package/dist/owned-browser.js +13 -1
  104. package/dist/owned-process.d.ts +2 -0
  105. package/dist/owned-process.d.ts.map +1 -1
  106. package/dist/owned-process.js +7 -3
  107. package/dist/playwright-runtime.d.ts +1 -1
  108. package/dist/playwright-runtime.d.ts.map +1 -1
  109. package/dist/playwright-runtime.js +8 -8
  110. package/dist/run-observability.d.ts +25 -0
  111. package/dist/run-observability.d.ts.map +1 -0
  112. package/dist/run-observability.js +115 -0
  113. package/dist/run-store.d.ts +274 -0
  114. package/dist/run-store.d.ts.map +1 -0
  115. package/dist/run-store.js +631 -0
  116. package/dist/runtime-metrics.d.ts +27 -0
  117. package/dist/runtime-metrics.d.ts.map +1 -0
  118. package/dist/runtime-metrics.js +66 -0
  119. package/dist/runtime-page-state.d.ts +11 -0
  120. package/dist/runtime-page-state.d.ts.map +1 -0
  121. package/dist/runtime-page-state.js +62 -0
  122. package/dist/runtime-protected-state.d.ts +16 -0
  123. package/dist/runtime-protected-state.d.ts.map +1 -0
  124. package/dist/runtime-protected-state.js +157 -0
  125. package/dist/runtime-state.d.ts +10 -44
  126. package/dist/runtime-state.d.ts.map +1 -1
  127. package/dist/runtime-state.js +57 -222
  128. package/dist/secrets/backend.d.ts +65 -16
  129. package/dist/secrets/backend.d.ts.map +1 -1
  130. package/dist/secrets/backend.js +135 -95
  131. package/dist/secrets/catalog-sync.d.ts.map +1 -1
  132. package/dist/secrets/catalog-sync.js +4 -1
  133. package/dist/secrets/form-matcher.d.ts +5 -5
  134. package/dist/secrets/form-matcher.d.ts.map +1 -1
  135. package/dist/secrets/form-matcher.js +292 -164
  136. package/dist/secrets/intent-output.d.ts +6 -10
  137. package/dist/secrets/intent-output.d.ts.map +1 -1
  138. package/dist/secrets/intent-output.js +4 -58
  139. package/dist/secrets/mock-agentpay-cabinet.d.ts +38 -27
  140. package/dist/secrets/mock-agentpay-cabinet.d.ts.map +1 -1
  141. package/dist/secrets/mock-agentpay-cabinet.js +177 -111
  142. package/dist/secrets/protected-artifact-guard.d.ts +2 -2
  143. package/dist/secrets/protected-artifact-guard.d.ts.map +1 -1
  144. package/dist/secrets/protected-artifact-guard.js +2 -2
  145. package/dist/secrets/protected-bindings.d.ts +1 -1
  146. package/dist/secrets/protected-bindings.d.ts.map +1 -1
  147. package/dist/secrets/protected-bindings.js +6 -0
  148. package/dist/secrets/protected-field-semantics.d.ts +9 -0
  149. package/dist/secrets/protected-field-semantics.d.ts.map +1 -0
  150. package/dist/secrets/protected-field-semantics.js +154 -0
  151. package/dist/secrets/protected-field-values.d.ts.map +1 -1
  152. package/dist/secrets/protected-field-values.js +3 -3
  153. package/dist/secrets/protected-fill.d.ts +1 -1
  154. package/dist/secrets/protected-fill.d.ts.map +1 -1
  155. package/dist/secrets/protected-fill.js +45 -149
  156. package/dist/secrets/protected-value-adapters.d.ts +2 -1
  157. package/dist/secrets/protected-value-adapters.d.ts.map +1 -1
  158. package/dist/secrets/protected-value-adapters.js +80 -1
  159. package/dist/secrets/request-output.d.ts +11 -0
  160. package/dist/secrets/request-output.d.ts.map +1 -0
  161. package/dist/secrets/request-output.js +75 -0
  162. package/dist/secrets/types.d.ts +15 -9
  163. package/dist/secrets/types.d.ts.map +1 -1
  164. package/dist/secrets/types.js +3 -0
  165. package/dist/session-event-exporter.d.ts +36 -0
  166. package/dist/session-event-exporter.d.ts.map +1 -0
  167. package/dist/session-event-exporter.js +428 -0
  168. package/dist/session.d.ts +16 -7
  169. package/dist/session.d.ts.map +1 -1
  170. package/dist/session.js +150 -23
  171. package/dist/sessions-backend.d.ts +354 -0
  172. package/dist/sessions-backend.d.ts.map +1 -0
  173. package/dist/sessions-backend.js +126 -0
  174. package/dist/solver/browser-launcher.d.ts +1 -1
  175. package/dist/solver/browser-launcher.d.ts.map +1 -1
  176. package/dist/solver/browser-launcher.js +39 -13
  177. package/dist/solver/captcha-solver.d.ts.map +1 -1
  178. package/dist/solver/captcha-solver.js +8 -1
  179. package/dist/solver/types.d.ts +1 -0
  180. package/dist/solver/types.d.ts.map +1 -1
  181. package/dist/workflow-session-completion.d.ts +33 -0
  182. package/dist/workflow-session-completion.d.ts.map +1 -0
  183. package/dist/workflow-session-completion.js +156 -0
  184. package/package.json +9 -1
  185. package/dist/commands/create-intent.d.ts +0 -6
  186. package/dist/commands/create-intent.d.ts.map +0 -1
  187. package/dist/commands/create-intent.js +0 -75
  188. package/dist/commands/poll-intent.d.ts +0 -6
  189. package/dist/commands/poll-intent.d.ts.map +0 -1
  190. package/dist/commands/poll-intent.js +0 -57
@@ -2,21 +2,28 @@
2
2
  * browse act <targetRef> <action> [value] — Perform a deterministic action on a stored target.
3
3
  */
4
4
  import { saveSession } from '../session.js';
5
- import { bumpPageScopeEpoch, clearProtectedExposure, getProtectedExposure, getSurface, getTarget, incrementMetric, locatorCandidateKey, markTargetLifecycle, recordActionResult, registerPage, setTargetAvailability, setCurrentPage, updateTarget, } from '../runtime-state.js';
5
+ import { getSurface, getTarget, markTargetLifecycle, setTargetAvailability, updateTarget, } from '../runtime-state.js';
6
+ import { incrementMetric, recordActionResult } from '../runtime-metrics.js';
7
+ import { bumpPageScopeEpoch, registerPage, setCurrentPage } from '../runtime-page-state.js';
8
+ import { clearProtectedExposure, getProtectedExposure } from '../runtime-protected-state.js';
6
9
  import { outputContractFailure, outputFailure, outputJSON } from '../output.js';
7
- import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, genericClickObservationChanged, locatorStateChanged, pageObservationChanged, rankLocatorCandidates, shouldVerifyObservableProgress, waitForAcceptanceProbe, } from './action-acceptance.js';
10
+ import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, genericClickObservationChanged, locatorStateChanged, pageObservationChanged, shouldVerifyObservableProgress, waitForAcceptanceProbe, } from './action-acceptance.js';
8
11
  import { captureActionFailureArtifacts, startActionTrace } from './action-artifacts.js';
9
- import { buildLocator, resolveLocatorRoot } from './action-fallbacks.js';
10
12
  import { clickActivationStrategyForTarget } from './click-activation-policy.js';
11
13
  import { resolveSubmitResult } from './action-result-resolution.js';
12
14
  import { projectActionValue } from './action-value-projection.js';
13
15
  import { applyActionWithFallbacks } from './action-executor.js';
14
16
  import { BROWSE_ACTIONS, isBrowseAction } from './browse-actions.js';
15
17
  import { isCompatibleMutableFieldBinding, normalizePageSignature, readLocatorBindingSnapshot, readLocatorDomSignature, } from './descriptor-validation.js';
18
+ import { assertStoredBindingStillValid, resolvePreparedLocatorCandidates, resolveInteractionRoots, targetUsesSurfaceAsPrimaryLocator, } from './interaction-kernel.js';
16
19
  import { isLocatorUserActionable } from './user-actionable.js';
17
20
  import { resolveSurfaceScopeRoot } from './target-resolution.js';
18
21
  import { buildProtectedArtifactsSuppressed } from '../secrets/protected-artifact-guard.js';
19
22
  import { connectPlaywright, disconnectPlaywright, listPages, resolvePageByRef, syncSessionPage, } from '../playwright-runtime.js';
23
+ import { finishRunStep, saveArtifactManifest, startRunStep } from '../run-store.js';
24
+ import { appendCommandLifecycleEventBestEffort, captureStepSnapshotBestEffort, } from '../run-observability.js';
25
+ import { withApiTraceContext } from '../command-api-tracing.js';
26
+ import { exportRunStepToOtlpHttpJsonBestEffort } from '../otel-exporter.js';
20
27
  function ensureValue(action, value) {
21
28
  if (action === 'click')
22
29
  return undefined;
@@ -25,7 +32,65 @@ function ensureValue(action, value) {
25
32
  throw new Error(`Act value is required for action: ${action}`);
26
33
  }
27
34
  const MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS = [0, 25, 50, 100];
28
- function emitActPreflightFailure(params) {
35
+ const PARTIAL_SELECTION_PROGRESS_MESSAGE = 'Text entered. Selection is not complete. Choose one option from the visible list.';
36
+ async function readExpandedState(locator) {
37
+ if (!locator) {
38
+ return null;
39
+ }
40
+ const state = await captureLocatorState(locator, ['expanded']).catch(() => null);
41
+ return typeof state?.expanded === 'boolean' ? state.expanded : null;
42
+ }
43
+ async function partialProgressForAliasedSelection(args) {
44
+ const { requestedAction, probe } = args;
45
+ if ((requestedAction !== 'fill' && requestedAction !== 'type') || probe?.policy !== 'selection') {
46
+ return null;
47
+ }
48
+ const readExpanded = await readExpandedState(probe.readLocator);
49
+ if (readExpanded === true) {
50
+ return {
51
+ outcomeType: 'partial_progress',
52
+ message: PARTIAL_SELECTION_PROGRESS_MESSAGE,
53
+ };
54
+ }
55
+ const fallbackExpanded = probe.readLocator === probe.locator ? readExpanded : await readExpandedState(probe.locator);
56
+ if (fallbackExpanded === true) {
57
+ return {
58
+ outcomeType: 'partial_progress',
59
+ message: PARTIAL_SELECTION_PROGRESS_MESSAGE,
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+ async function emitActPreflightFailure(params) {
65
+ finalizeActStepBestEffort(params.runId, params.stepId, {
66
+ success: false,
67
+ outcomeType: params.outcomeType,
68
+ message: params.message,
69
+ reason: params.reason,
70
+ });
71
+ const step = params.runId && params.stepId
72
+ ? {
73
+ runId: params.runId,
74
+ stepId: params.stepId,
75
+ command: 'act',
76
+ }
77
+ : null;
78
+ captureStepSnapshotBestEffort({
79
+ session: params.session,
80
+ step,
81
+ phase: 'point-in-time',
82
+ pageRef: params.session.runtime?.currentPageRef,
83
+ });
84
+ appendCommandLifecycleEventBestEffort({
85
+ step,
86
+ phase: 'failed',
87
+ attributes: {
88
+ outcomeType: params.outcomeType,
89
+ targetRef: params.targetRef,
90
+ reason: params.reason,
91
+ },
92
+ });
93
+ await exportRunStepToOtlpHttpJsonBestEffort(params.runId, params.stepId);
29
94
  return outputContractFailure({
30
95
  error: params.error,
31
96
  outcomeType: params.outcomeType,
@@ -120,6 +185,81 @@ function redactNoObservableProgressDiagnosis(diagnosis) {
120
185
  : {}),
121
186
  };
122
187
  }
188
+ function buildActArtifactManifest(params) {
189
+ if ('suppressed' in params.artifacts) {
190
+ return {
191
+ artifactManifestId: `${params.stepId}-artifacts`,
192
+ stepId: params.stepId,
193
+ screenshots: [],
194
+ htmlSnapshots: [],
195
+ traces: [],
196
+ logs: [],
197
+ suppressed: [
198
+ { kind: 'screenshot', reason: 'protected_exposure_active' },
199
+ { kind: 'html', reason: 'protected_exposure_active' },
200
+ { kind: 'trace', reason: 'protected_exposure_active' },
201
+ { kind: 'log', reason: 'protected_exposure_active' },
202
+ ],
203
+ };
204
+ }
205
+ return {
206
+ artifactManifestId: `${params.stepId}-artifacts`,
207
+ stepId: params.stepId,
208
+ screenshots: params.artifacts.screenshotPath
209
+ ? [{ path: params.artifacts.screenshotPath, purpose: 'failure_screenshot' }]
210
+ : [],
211
+ htmlSnapshots: params.artifacts.htmlPath
212
+ ? [{ path: params.artifacts.htmlPath, purpose: 'failure_html_snapshot' }]
213
+ : [],
214
+ traces: params.artifacts.tracePath
215
+ ? [{ path: params.artifacts.tracePath, purpose: 'failure_trace' }]
216
+ : [],
217
+ logs: [{ path: params.artifacts.actionLogPath, purpose: 'failure_action_log' }],
218
+ suppressed: [],
219
+ };
220
+ }
221
+ function persistActArtifactManifestBestEffort(runId, stepId, artifacts) {
222
+ if (!runId || !stepId || !artifacts) {
223
+ return undefined;
224
+ }
225
+ try {
226
+ const manifest = buildActArtifactManifest({
227
+ stepId,
228
+ artifacts,
229
+ });
230
+ saveArtifactManifest(runId, manifest);
231
+ return manifest.artifactManifestId;
232
+ }
233
+ catch {
234
+ return undefined;
235
+ }
236
+ }
237
+ function buildActSnapshotArtifactRefs(artifacts) {
238
+ if (!artifacts || 'suppressed' in artifacts) {
239
+ return undefined;
240
+ }
241
+ return {
242
+ ...(artifacts.screenshotPath ? { screenshotPath: artifacts.screenshotPath } : {}),
243
+ ...(artifacts.htmlPath ? { htmlPath: artifacts.htmlPath } : {}),
244
+ ...(artifacts.tracePath ? { tracePath: artifacts.tracePath } : {}),
245
+ ...(artifacts.actionLogPath ? { logPath: artifacts.actionLogPath } : {}),
246
+ };
247
+ }
248
+ function finalizeActStepBestEffort(runId, stepId, options) {
249
+ if (!runId || !stepId) {
250
+ return;
251
+ }
252
+ try {
253
+ finishRunStep({
254
+ runId,
255
+ stepId,
256
+ ...options,
257
+ });
258
+ }
259
+ catch {
260
+ // Best effort only.
261
+ }
262
+ }
123
263
  function isEditableLikeTarget(target) {
124
264
  if (target.controlFamily === 'text-input' ||
125
265
  target.controlFamily === 'select' ||
@@ -148,136 +288,6 @@ function shouldDeferSurfaceResolutionForEditablePress(target, action) {
148
288
  isEditableLikeTarget(target) &&
149
289
  !target.locatorCandidates.some((candidate) => candidate.scope === 'surface'));
150
290
  }
151
- function targetUsesSurfaceAsPrimaryLocator(target, surface) {
152
- const surfaceCandidates = new Set(surface.locatorCandidates.map(locatorCandidateKey));
153
- return target.locatorCandidates.some((candidate) => surfaceCandidates.has(locatorCandidateKey(candidate)));
154
- }
155
- async function resolveActionRoot(page, target, attempts, surface) {
156
- const baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
157
- if (!surface) {
158
- return {
159
- baseRoot,
160
- locatorRoot: baseRoot,
161
- surfaceRoot: null,
162
- };
163
- }
164
- const surfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
165
- if (surfaceRoot) {
166
- if (targetUsesSurfaceAsPrimaryLocator(target, surface)) {
167
- attempts.push('surface.resolve.self-target');
168
- return {
169
- baseRoot,
170
- locatorRoot: baseRoot,
171
- surfaceRoot,
172
- };
173
- }
174
- return {
175
- baseRoot,
176
- locatorRoot: surfaceRoot,
177
- surfaceRoot,
178
- };
179
- }
180
- attempts.push('surface.resolve.fallback:page');
181
- return {
182
- baseRoot,
183
- locatorRoot: baseRoot,
184
- surfaceRoot: null,
185
- };
186
- }
187
- function resolveLocatorRootForCandidate(baseRoot, defaultRoot, surfaceRoot, candidate) {
188
- if (candidate.scope === 'root') {
189
- return baseRoot;
190
- }
191
- if (candidate.scope === 'surface') {
192
- return surfaceRoot;
193
- }
194
- return defaultRoot;
195
- }
196
- async function prepareLocatorForAction(locator, action, strategy, attempts, options) {
197
- const count = await locator.count().catch(() => 0);
198
- if (count === 0) {
199
- attempts.push(`resolve.skip:${strategy}:empty`);
200
- return { locator: null };
201
- }
202
- if (action !== 'click' && count > 1) {
203
- attempts.push(`resolve.skip:${strategy}:ambiguous:${count}`);
204
- return { locator: null };
205
- }
206
- let resolvedLocator = locator.first();
207
- if (action === 'click' && count > 1) {
208
- const visibleCandidates = [];
209
- for (let index = 0; index < count; index += 1) {
210
- const candidate = locator.nth(index);
211
- const visible = await isLocatorUserActionable(candidate);
212
- if (!visible) {
213
- continue;
214
- }
215
- visibleCandidates.push(candidate);
216
- }
217
- if (visibleCandidates.length === 1) {
218
- attempts.push(`resolve.visible-unique:${strategy}`);
219
- resolvedLocator = visibleCandidates[0] ?? locator.first();
220
- }
221
- else if (visibleCandidates.length > 1) {
222
- attempts.push(`resolve.skip:${strategy}:ambiguous-visible:${visibleCandidates.length}`);
223
- return { locator: null };
224
- }
225
- else {
226
- attempts.push(`resolve.skip:${strategy}:hidden`);
227
- return { locator: null };
228
- }
229
- }
230
- const visible = await isLocatorUserActionable(resolvedLocator);
231
- if (!visible) {
232
- attempts.push(`resolve.skip:${strategy}:hidden`);
233
- return { locator: null };
234
- }
235
- const disabled = await resolvedLocator.isDisabled?.().catch(() => false);
236
- if (disabled) {
237
- attempts.push(`resolve.skip:${strategy}:disabled`);
238
- return { locator: null, blockedReason: 'disabled' };
239
- }
240
- if (action === 'fill' || action === 'type' || options?.allowDescendantPressFallback) {
241
- const editable = await resolvedLocator.isEditable().catch(() => false);
242
- if (!editable) {
243
- const descendantSelector = options?.allowDescendantPressFallback
244
- ? 'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]'
245
- : 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]';
246
- const descendantCandidates = resolvedLocator.locator(descendantSelector);
247
- const descendantCount = await descendantCandidates.count().catch(() => 0);
248
- const candidateDescendants = [];
249
- for (let index = 0; index < descendantCount; index += 1) {
250
- const descendant = descendantCandidates.nth(index);
251
- const descendantVisible = await isLocatorUserActionable(descendant);
252
- if (!descendantVisible)
253
- continue;
254
- if (!options?.allowDescendantPressFallback || action === 'fill' || action === 'type') {
255
- const descendantEditable = await descendant.isEditable().catch(() => false);
256
- if (!descendantEditable)
257
- continue;
258
- }
259
- candidateDescendants.push(descendant);
260
- }
261
- if (candidateDescendants.length === 1) {
262
- attempts.push(options?.allowDescendantPressFallback && action === 'press'
263
- ? `resolve.descendant-press:${strategy}`
264
- : `resolve.descendant-editable:${strategy}`);
265
- return { locator: candidateDescendants[0] ?? null };
266
- }
267
- if (candidateDescendants.length > 1) {
268
- attempts.push(`resolve.skip:${strategy}:descendant-ambiguous:${candidateDescendants.length}`);
269
- return { locator: null };
270
- }
271
- if (options?.allowReadonlyFallback) {
272
- attempts.push(`resolve.readonly-fallback:${strategy}`);
273
- return { locator: resolvedLocator };
274
- }
275
- attempts.push(`resolve.skip:${strategy}:readonly`);
276
- return { locator: null, blockedReason: 'readonly' };
277
- }
278
- }
279
- return { locator: resolvedLocator };
280
- }
281
291
  async function recoverLocatorFromSurfaceRoot(locatorRoot, target, action, attempts) {
282
292
  if (action !== 'press' ||
283
293
  !(target.controlFamily === 'text-input' ||
@@ -340,599 +350,741 @@ async function waitForLatePage(browser, beforePages, timeoutMs = 2_000) {
340
350
  return null;
341
351
  }
342
352
  export async function act(session, targetRef, action, value) {
343
- const target = getTarget(session, targetRef);
344
- if (!target) {
345
- return emitActPreflightFailure({
346
- error: 'unknown_target_ref',
347
- outcomeType: 'blocked',
348
- message: 'The requested targetRef is unknown.',
349
- reason: `No stored target matches targetRef ${targetRef}.`,
350
- targetRef,
351
- action,
352
- });
353
- }
354
- if (target.lifecycle !== 'live') {
355
- return emitActPreflightFailure({
356
- error: 'stale_target_ref',
357
- outcomeType: 'binding_stale',
358
- message: 'The requested target is no longer live.',
359
- reason: `Target ${targetRef} is ${target.lifecycle}${target.lifecycleReason ? ` because ${target.lifecycleReason}` : ''}.`,
360
- targetRef,
361
- action,
362
- });
363
- }
364
- if (target.capability !== 'actionable') {
365
- return emitActPreflightFailure({
366
- error: 'target_not_actionable',
367
- outcomeType: 'unsupported',
368
- message: 'The requested target cannot be used for actions.',
369
- reason: `Target ${targetRef} has capability ${target.capability}, not actionable.`,
370
- targetRef,
371
- action,
372
- });
373
- }
374
- if (!target.allowedActions.includes(action)) {
375
- return emitActPreflightFailure({
376
- error: 'action_not_allowed_for_target',
377
- outcomeType: 'unsupported',
378
- message: 'The requested action is not allowed for this target.',
379
- reason: `Target ${targetRef} allows ${target.allowedActions.join(', ')}, not ${action}.`,
380
- targetRef,
381
- action,
382
- });
383
- }
384
- if (target.availability.state === 'gated' &&
385
- (target.availability.reason === 'occupied' || target.availability.reason === 'not-selectable')) {
386
- return emitActPreflightFailure({
387
- error: 'target_gated',
388
- outcomeType: 'blocked',
389
- message: 'The requested target is currently gated.',
390
- reason: `Target ${targetRef} is gated${target.availability.reason ? ` because ${target.availability.reason}` : ''}.`,
391
- targetRef,
392
- action,
393
- });
394
- }
395
- const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
396
- if (surface && surface.lifecycle !== 'live') {
397
- setTargetAvailability(session, targetRef, 'surface-inactive', surface.lifecycleReason ?? `surface-${surface.lifecycle}`);
398
- saveSession(session);
399
- return emitActPreflightFailure({
400
- error: 'target_surface_not_live',
401
- outcomeType: 'blocked',
402
- message: 'The requested target surface is no longer live.',
403
- reason: `Surface ${surface.ref} is ${surface.lifecycle}${surface.lifecycleReason ? ` because ${surface.lifecycleReason}` : ''}.`,
404
- targetRef,
405
- action,
406
- });
407
- }
408
- if (surface && surface.availability.state !== 'available') {
409
- setTargetAvailability(session, targetRef, 'surface-inactive', surface.availability.reason ?? `surface-${surface.availability.state}`);
410
- saveSession(session);
411
- return emitActPreflightFailure({
412
- error: 'target_surface_unavailable',
413
- outcomeType: 'blocked',
414
- message: 'The requested target surface is not currently available.',
415
- reason: `Surface ${surface.ref} is ${surface.availability.state}${surface.availability.reason ? ` because ${surface.availability.reason}` : ''}.`,
416
- targetRef,
417
- action,
418
- });
419
- }
420
- const actionValue = ensureValue(action, value);
421
- const attempts = [];
422
- const startedAt = Date.now();
423
- let browser = null;
424
- let failureMessage = null;
425
- let failureArtifacts;
426
- let currentPage = null;
427
- let currentPageRef = target.pageRef;
428
- const startingPageUrl = session.runtime?.pages?.[target.pageRef]?.url ?? null;
429
- const protectedExposureAtStart = getProtectedExposure(session, target.pageRef);
430
- let locatorStrategy = null;
431
- let recoveredAfterError = false;
432
- let recoveredAcceptancePolicy = null;
433
- let staleReason = null;
434
- let progressProbe = null;
435
- let noProgressDiagnosis = null;
436
- let liveTarget = target;
437
- let trace = {
438
- finishSuccess: async () => { },
439
- finishFailure: async (_artifactDir) => undefined,
440
- };
441
- try {
442
- browser = await connectPlaywright(session.cdpUrl);
443
- }
444
- catch (err) {
445
- return emitActPreflightFailure({
446
- error: 'browser_connection_failed',
447
- outcomeType: 'blocked',
448
- message: 'The action could not start because AgentBrowse failed to connect to the browser.',
449
- reason: err instanceof Error ? err.message : String(err),
353
+ const requestedAction = action;
354
+ const runId = session.activeRunId;
355
+ const actStep = runId
356
+ ? startRunStep({
357
+ runId,
358
+ command: 'act',
359
+ input: {
360
+ targetRef,
361
+ action: requestedAction,
362
+ valueSupplied: typeof value === 'string' && value.length > 0,
363
+ },
364
+ refs: {
365
+ targetRef,
366
+ },
367
+ })
368
+ : null;
369
+ captureStepSnapshotBestEffort({
370
+ session,
371
+ step: actStep,
372
+ phase: 'before',
373
+ pageRef: session.runtime?.currentPageRef,
374
+ });
375
+ appendCommandLifecycleEventBestEffort({
376
+ step: actStep,
377
+ phase: 'started',
378
+ attributes: {
450
379
  targetRef,
451
- action,
452
- });
453
- }
454
- try {
455
- const page = await resolvePageByRef(browser, session, target.pageRef);
456
- currentPage = page;
457
- setCurrentPage(session, target.pageRef);
458
- const { url } = await syncSessionPage(session, target.pageRef, page);
459
- trace = await startActionTrace(page, {
460
- suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
461
- });
462
- if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
463
- staleReason = 'page-signature-mismatch';
464
- throw new Error('stale_target_page_signature_changed');
380
+ action: requestedAction,
381
+ valueSupplied: typeof value === 'string' && value.length > 0,
382
+ },
383
+ });
384
+ return withApiTraceContext({
385
+ runId,
386
+ stepId: actStep?.stepId,
387
+ command: 'act',
388
+ }, async () => {
389
+ const target = getTarget(session, targetRef);
390
+ if (!target) {
391
+ return emitActPreflightFailure({
392
+ session,
393
+ error: 'unknown_target_ref',
394
+ outcomeType: 'blocked',
395
+ message: 'The requested targetRef is unknown.',
396
+ reason: `No stored target matches targetRef ${targetRef}.`,
397
+ targetRef,
398
+ action,
399
+ runId,
400
+ stepId: actStep?.stepId,
401
+ });
465
402
  }
466
- const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
467
- if (!liveTarget.domSignature) {
468
- return false;
403
+ if (target.lifecycle !== 'live') {
404
+ return emitActPreflightFailure({
405
+ session,
406
+ error: 'stale_target_ref',
407
+ outcomeType: 'binding_stale',
408
+ message: 'The requested target is no longer live.',
409
+ reason: `Target ${targetRef} is ${target.lifecycle}${target.lifecycleReason ? ` because ${target.lifecycleReason}` : ''}.`,
410
+ targetRef,
411
+ action,
412
+ runId,
413
+ stepId: actStep?.stepId,
414
+ });
415
+ }
416
+ if (target.capability !== 'actionable') {
417
+ return emitActPreflightFailure({
418
+ session,
419
+ error: 'target_not_actionable',
420
+ outcomeType: 'unsupported',
421
+ message: 'The requested target cannot be used for actions.',
422
+ reason: `Target ${targetRef} has capability ${target.capability}, not actionable.`,
423
+ targetRef,
424
+ action: requestedAction,
425
+ runId,
426
+ stepId: actStep?.stepId,
427
+ });
428
+ }
429
+ if (!target.allowedActions.includes(action)) {
430
+ const canAliasFillLikeToSelect = (requestedAction === 'fill' || requestedAction === 'type') &&
431
+ target.controlFamily === 'select' &&
432
+ target.allowedActions.includes('select') &&
433
+ typeof value === 'string' &&
434
+ value.length > 0;
435
+ if (canAliasFillLikeToSelect) {
436
+ action = 'select';
469
437
  }
470
- for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
471
- const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
472
- if (attemptIndex > 0) {
473
- attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
474
- await new Promise((resolve) => setTimeout(resolve, delayMs));
475
- }
476
- const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
477
- if (!snapshot?.domSignature) {
478
- continue;
479
- }
480
- if (snapshot.domSignature === liveTarget.domSignature) {
481
- return false;
482
- }
483
- if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
484
- continue;
485
- }
486
- attempts.push(`domSignature.rebound:${strategy}`);
487
- const updatedTarget = updateTarget(session, targetRef, {
488
- domSignature: snapshot.domSignature,
489
- label: snapshot.label ?? liveTarget.label,
490
- lifecycle: 'live',
491
- lifecycleReason: undefined,
492
- availability: { state: 'available' },
493
- semantics: {
494
- ...liveTarget.semantics,
495
- name: snapshot.label ?? liveTarget.semantics?.name,
496
- role: snapshot.role ?? liveTarget.semantics?.role,
497
- },
438
+ else {
439
+ return emitActPreflightFailure({
440
+ session,
441
+ error: 'action_not_allowed_for_target',
442
+ outcomeType: 'unsupported',
443
+ message: 'The requested action is not allowed for this target.',
444
+ reason: `Target ${targetRef} allows ${target.allowedActions.join(', ')}, not ${requestedAction}.`,
445
+ targetRef,
446
+ action: requestedAction,
498
447
  });
499
- if (updatedTarget) {
500
- liveTarget = updatedTarget;
501
- }
502
- return true;
503
448
  }
504
- return false;
449
+ }
450
+ if (target.availability.state === 'gated' &&
451
+ (target.availability.reason === 'occupied' || target.availability.reason === 'not-selectable')) {
452
+ return emitActPreflightFailure({
453
+ session,
454
+ error: 'target_gated',
455
+ outcomeType: 'blocked',
456
+ message: 'The requested target is currently gated.',
457
+ reason: `Target ${targetRef} is gated${target.availability.reason ? ` because ${target.availability.reason}` : ''}.`,
458
+ targetRef,
459
+ action: requestedAction,
460
+ });
461
+ }
462
+ const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
463
+ if (surface && surface.lifecycle !== 'live') {
464
+ setTargetAvailability(session, targetRef, 'surface-inactive', surface.lifecycleReason ?? `surface-${surface.lifecycle}`);
465
+ saveSession(session);
466
+ return emitActPreflightFailure({
467
+ session,
468
+ error: 'target_surface_not_live',
469
+ outcomeType: 'blocked',
470
+ message: 'The requested target surface is no longer live.',
471
+ reason: `Surface ${surface.ref} is ${surface.lifecycle}${surface.lifecycleReason ? ` because ${surface.lifecycleReason}` : ''}.`,
472
+ targetRef,
473
+ action: requestedAction,
474
+ });
475
+ }
476
+ if (surface && surface.availability.state !== 'available') {
477
+ setTargetAvailability(session, targetRef, 'surface-inactive', surface.availability.reason ?? `surface-${surface.availability.state}`);
478
+ saveSession(session);
479
+ return emitActPreflightFailure({
480
+ session,
481
+ error: 'target_surface_unavailable',
482
+ outcomeType: 'blocked',
483
+ message: 'The requested target surface is not currently available.',
484
+ reason: `Surface ${surface.ref} is ${surface.availability.state}${surface.availability.reason ? ` because ${surface.availability.reason}` : ''}.`,
485
+ targetRef,
486
+ action: requestedAction,
487
+ });
488
+ }
489
+ const actionValue = ensureValue(action, value);
490
+ const attempts = [];
491
+ if (action !== requestedAction) {
492
+ attempts.push(`action.alias:${requestedAction}->${action}`);
493
+ }
494
+ const startedAt = Date.now();
495
+ let browser = null;
496
+ let failureMessage = null;
497
+ let failureArtifacts;
498
+ let currentPage = null;
499
+ let currentPageRef = target.pageRef;
500
+ const startingPageUrl = session.runtime?.pages?.[target.pageRef]?.url ?? null;
501
+ const protectedExposureAtStart = getProtectedExposure(session, target.pageRef);
502
+ let locatorStrategy = null;
503
+ let recoveredAfterError = false;
504
+ let recoveredAcceptancePolicy = null;
505
+ let recoveredProgressProbe = null;
506
+ let staleReason = null;
507
+ let progressProbe = null;
508
+ let noProgressDiagnosis = null;
509
+ let partialProgressResult = null;
510
+ let liveTarget = target;
511
+ let trace = {
512
+ finishSuccess: async () => { },
513
+ finishFailure: async (_artifactDir) => undefined,
505
514
  };
506
- const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
507
- if (liveTarget.pageSignature &&
508
- normalizePageSignature(page.url()) !== liveTarget.pageSignature) {
509
- attempts.push(`stale.page-signature:${stage}`);
515
+ try {
516
+ browser = await connectPlaywright(session.cdpUrl);
517
+ }
518
+ catch (err) {
519
+ return emitActPreflightFailure({
520
+ session,
521
+ error: 'browser_connection_failed',
522
+ outcomeType: 'blocked',
523
+ message: 'The action could not start because AgentBrowse failed to connect to the browser.',
524
+ reason: err instanceof Error ? err.message : String(err),
525
+ targetRef,
526
+ action,
527
+ });
528
+ }
529
+ try {
530
+ const page = await resolvePageByRef(browser, session, target.pageRef);
531
+ currentPage = page;
532
+ setCurrentPage(session, target.pageRef);
533
+ const { url } = await syncSessionPage(session, target.pageRef, page);
534
+ trace = await startActionTrace(page, {
535
+ suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
536
+ });
537
+ if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
510
538
  staleReason = 'page-signature-mismatch';
511
539
  throw new Error('stale_target_page_signature_changed');
512
540
  }
513
- const liveCount = await resolvedLocator.count().catch(() => 0);
514
- if (liveCount === 0) {
515
- attempts.push(`stale.locator:${stage}`);
516
- staleReason = 'locator-resolution-failed';
517
- throw new Error('stale_target_locator_resolution_failed');
518
- }
519
- if (liveTarget.domSignature) {
520
- const rebound = await tryRebindMutableFieldTarget(resolvedLocator, stage);
521
- if (!rebound) {
522
- const liveSignature = await readLocatorDomSignature(resolvedLocator).catch(() => null);
523
- if (liveSignature && liveSignature !== liveTarget.domSignature) {
524
- attempts.push(`stale.dom-signature:${stage}`);
525
- staleReason = 'dom-signature-mismatch';
526
- throw new Error('stale_target_dom_signature_changed');
527
- }
528
- }
529
- }
530
- };
531
- let resolvedBy = null;
532
- const beforePages = listPages(browser);
533
- const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
534
- const beforePageObservation = shouldCheckProgress ? await capturePageObservation(page) : null;
535
- const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
536
- let baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
537
- let locatorRoot = baseRoot;
538
- let surfaceRoot = null;
539
- if (!deferSurfaceResolution) {
540
- ({ baseRoot, locatorRoot, surfaceRoot } = await resolveActionRoot(page, target, attempts, surface));
541
- }
542
- let lastError = null;
543
- let sawDomSignatureMismatch = false;
544
- let sawDisabledTarget = false;
545
- let sawReadonlyTarget = false;
546
- const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
547
- if (!options?.skipDomSignature && liveTarget.domSignature) {
548
- const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
549
- if (!rebound) {
550
- const liveSignature = await readLocatorDomSignature(resolvedLocator);
551
- if (liveSignature && liveSignature !== liveTarget.domSignature) {
552
- attempts.push(`domSignature.mismatch:${strategy}`);
553
- sawDomSignatureMismatch = true;
554
- return false;
555
- }
556
- }
557
- }
558
- let acceptanceProbe = null;
559
- const tryRecoverActionErrorAcceptance = async () => {
560
- if (!acceptanceProbe) {
561
- return false;
562
- }
563
- const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
564
- if (acceptance?.polls && acceptance.polls > 1) {
565
- attempts.push(`acceptance.polled:${acceptance.polls}`);
566
- }
567
- if (!acceptance?.accepted) {
541
+ const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
542
+ if (!liveTarget.domSignature) {
568
543
  return false;
569
544
  }
570
- if (acceptanceProbe.policy === 'submit') {
571
- const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
572
- if (!submitResolution.acceptAsProgress) {
545
+ for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
546
+ const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
547
+ if (attemptIndex > 0) {
548
+ attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
549
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
550
+ }
551
+ const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
552
+ if (!snapshot?.domSignature) {
553
+ continue;
554
+ }
555
+ if (snapshot.domSignature === liveTarget.domSignature) {
573
556
  return false;
574
557
  }
575
- attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
576
- if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
577
- attempts.push('submit-resolution:soft-result-candidate');
558
+ if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
559
+ continue;
578
560
  }
561
+ attempts.push(`domSignature.rebound:${strategy}`);
562
+ const updatedTarget = updateTarget(session, targetRef, {
563
+ domSignature: snapshot.domSignature,
564
+ label: snapshot.label ?? liveTarget.label,
565
+ lifecycle: 'live',
566
+ lifecycleReason: undefined,
567
+ availability: { state: 'available' },
568
+ semantics: {
569
+ ...liveTarget.semantics,
570
+ name: snapshot.label ?? liveTarget.semantics?.name,
571
+ role: snapshot.role ?? liveTarget.semantics?.role,
572
+ },
573
+ });
574
+ if (updatedTarget) {
575
+ liveTarget = updatedTarget;
576
+ }
577
+ return true;
579
578
  }
580
- attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
581
- incrementMetric(session, 'fallbackActions');
582
- resolvedBy = 'playwright-locator';
583
- locatorStrategy = strategy;
584
- progressProbe = null;
585
- lastError = null;
586
- recoveredAfterError = true;
587
- recoveredAcceptancePolicy = acceptanceProbe.policy;
588
- setTargetAvailability(session, targetRef, 'available');
589
- return true;
579
+ return false;
590
580
  };
591
- try {
592
- const valueProjection = await projectActionValue({
593
- target,
594
- action,
595
- actionValue,
596
- locator: resolvedLocator,
597
- attempts,
598
- });
599
- const executionValue = valueProjection?.executionValue ?? actionValue;
600
- const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
601
- acceptanceProbe = await createAcceptanceProbe({
602
- session,
603
- page,
604
- target,
605
- action,
606
- actionValue: acceptanceValue,
607
- locator: resolvedLocator,
608
- beforePageObservation,
609
- });
610
- attempts.push(`resolve:${strategy}`);
611
- const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
612
- clickActivationStrategy: clickActivationStrategyForTarget(target, action),
613
- guards: {
614
- assertStillValid: async (stage) => {
615
- await assertResolvedTargetStillValid(resolvedLocator, stage);
616
- },
581
+ const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
582
+ await assertStoredBindingStillValid(page, resolvedLocator, liveTarget, stage, {
583
+ onReason: async (reason, staleStage) => {
584
+ switch (reason) {
585
+ case 'page_signature_mismatch':
586
+ attempts.push(`stale.page-signature:${staleStage}`);
587
+ staleReason = 'page-signature-mismatch';
588
+ return false;
589
+ case 'locator_resolution_failed':
590
+ attempts.push(`stale.locator:${staleStage}`);
591
+ staleReason = 'locator-resolution-failed';
592
+ return false;
593
+ case 'dom_signature_mismatch': {
594
+ const rebound = await tryRebindMutableFieldTarget(resolvedLocator, staleStage);
595
+ if (rebound) {
596
+ return true;
597
+ }
598
+ attempts.push(`stale.dom-signature:${staleStage}`);
599
+ staleReason = 'dom-signature-mismatch';
600
+ return false;
601
+ }
602
+ }
603
+ },
604
+ errorForReason: (reason) => {
605
+ switch (reason) {
606
+ case 'page_signature_mismatch':
607
+ return 'stale_target_page_signature_changed';
608
+ case 'locator_resolution_failed':
609
+ return 'stale_target_locator_resolution_failed';
610
+ case 'dom_signature_mismatch':
611
+ return 'stale_target_dom_signature_changed';
612
+ }
617
613
  },
618
614
  });
619
- if (usedFallback) {
620
- incrementMetric(session, 'fallbackActions');
621
- }
622
- resolvedBy = 'playwright-locator';
623
- locatorStrategy = strategy;
624
- progressProbe = acceptanceProbe;
625
- setTargetAvailability(session, targetRef, 'available');
626
- return true;
615
+ };
616
+ let resolvedBy = null;
617
+ const beforePages = listPages(browser);
618
+ const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
619
+ const beforePageObservation = shouldCheckProgress ? await capturePageObservation(page) : null;
620
+ const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
621
+ let baseRoot = page;
622
+ let locatorRoot = baseRoot;
623
+ let surfaceRoot = null;
624
+ if (!deferSurfaceResolution) {
625
+ ({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
626
+ recordSelfTargetReuse: true,
627
+ }));
627
628
  }
628
- catch (err) {
629
- lastError = err instanceof Error ? err : new Error(String(err));
630
- const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
631
- (acceptanceProbe.policy === 'value-change' ||
632
- acceptanceProbe.policy === 'selection' ||
633
- acceptanceProbe.policy === 'date-selection' ||
634
- acceptanceProbe.policy === 'navigation' ||
635
- acceptanceProbe.policy === 'submit');
636
- if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
637
- return true;
638
- }
639
- if (staleReason) {
640
- throw lastError;
629
+ let lastError = null;
630
+ let sawDomSignatureMismatch = false;
631
+ let sawDisabledTarget = false;
632
+ let sawReadonlyTarget = false;
633
+ const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
634
+ if (!options?.skipDomSignature && liveTarget.domSignature) {
635
+ const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
636
+ if (!rebound) {
637
+ const liveSignature = await readLocatorDomSignature(resolvedLocator);
638
+ if (liveSignature && liveSignature !== liveTarget.domSignature) {
639
+ attempts.push(`domSignature.mismatch:${strategy}`);
640
+ sawDomSignatureMismatch = true;
641
+ return false;
642
+ }
643
+ }
641
644
  }
645
+ let acceptanceProbe = null;
646
+ const tryRecoverActionErrorAcceptance = async () => {
647
+ if (!acceptanceProbe) {
648
+ return false;
649
+ }
650
+ const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
651
+ if (acceptance?.polls && acceptance.polls > 1) {
652
+ attempts.push(`acceptance.polled:${acceptance.polls}`);
653
+ }
654
+ if (!acceptance?.accepted) {
655
+ return false;
656
+ }
657
+ if (acceptanceProbe.policy === 'submit') {
658
+ const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
659
+ if (!submitResolution.acceptAsProgress) {
660
+ return false;
661
+ }
662
+ attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
663
+ if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
664
+ attempts.push('submit-resolution:soft-result-candidate');
665
+ }
666
+ }
667
+ attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
668
+ incrementMetric(session, 'fallbackActions');
669
+ resolvedBy = 'playwright-locator';
670
+ locatorStrategy = strategy;
671
+ recoveredProgressProbe = acceptanceProbe;
672
+ progressProbe = null;
673
+ lastError = null;
674
+ recoveredAfterError = true;
675
+ recoveredAcceptancePolicy = acceptanceProbe.policy;
676
+ setTargetAvailability(session, targetRef, 'available');
677
+ return true;
678
+ };
642
679
  try {
643
- await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
680
+ const valueProjection = await projectActionValue({
681
+ target,
682
+ action,
683
+ actionValue,
684
+ locator: resolvedLocator,
685
+ attempts,
686
+ });
687
+ const executionValue = valueProjection?.executionValue ?? actionValue;
688
+ const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
689
+ acceptanceProbe = await createAcceptanceProbe({
690
+ session,
691
+ page,
692
+ target,
693
+ action,
694
+ actionValue: acceptanceValue,
695
+ locator: resolvedLocator,
696
+ beforePageObservation,
697
+ });
698
+ attempts.push(`resolve:${strategy}`);
699
+ const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
700
+ clickActivationStrategy: clickActivationStrategyForTarget(target, action),
701
+ guards: {
702
+ assertStillValid: async (stage) => {
703
+ await assertResolvedTargetStillValid(resolvedLocator, stage);
704
+ },
705
+ },
706
+ });
707
+ if (usedFallback) {
708
+ incrementMetric(session, 'fallbackActions');
709
+ }
710
+ resolvedBy = 'playwright-locator';
711
+ locatorStrategy = strategy;
712
+ progressProbe = acceptanceProbe;
713
+ setTargetAvailability(session, targetRef, 'available');
714
+ return true;
644
715
  }
645
- catch (validationError) {
646
- if ((acceptanceProbe?.policy === 'navigation' || acceptanceProbe?.policy === 'submit') &&
647
- validationError instanceof Error &&
648
- validationError.message === 'stale_target_page_signature_changed' &&
649
- (await tryRecoverActionErrorAcceptance())) {
716
+ catch (err) {
717
+ lastError = err instanceof Error ? err : new Error(String(err));
718
+ const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
719
+ (acceptanceProbe.policy === 'value-change' ||
720
+ acceptanceProbe.policy === 'selection' ||
721
+ acceptanceProbe.policy === 'date-selection' ||
722
+ acceptanceProbe.policy === 'navigation' ||
723
+ acceptanceProbe.policy === 'submit');
724
+ if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
650
725
  return true;
651
726
  }
652
- throw validationError;
653
- }
654
- return false;
655
- }
656
- };
657
- const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
658
- const popupPromise = watchForNewPage
659
- ? waitForPopup(page.context())
660
- : Promise.resolve(null);
661
- const tryRankedCandidates = async () => {
662
- for (const candidate of rankLocatorCandidates(target.locatorCandidates, action)) {
663
- const candidateRoot = resolveLocatorRootForCandidate(baseRoot, locatorRoot, surfaceRoot, candidate);
664
- if (!candidateRoot) {
665
- attempts.push(`resolve.skip:${candidate.strategy}:surface-unavailable`);
666
- continue;
727
+ if (staleReason) {
728
+ throw lastError;
729
+ }
730
+ try {
731
+ await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
732
+ }
733
+ catch (validationError) {
734
+ if ((acceptanceProbe?.policy === 'navigation' || acceptanceProbe?.policy === 'submit') &&
735
+ validationError instanceof Error &&
736
+ validationError.message === 'stale_target_page_signature_changed' &&
737
+ (await tryRecoverActionErrorAcceptance())) {
738
+ return true;
739
+ }
740
+ throw validationError;
741
+ }
742
+ return false;
667
743
  }
668
- const locator = buildLocator(candidateRoot, candidate);
669
- if (!locator)
670
- continue;
671
- const preparedLocator = await prepareLocatorForAction(locator, action, candidate.strategy, attempts, {
672
- allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
673
- allowDescendantPressFallback: action === 'press' &&
674
- (target.controlFamily === 'text-input' ||
675
- target.controlFamily === 'select' ||
676
- target.controlFamily === 'datepicker'),
744
+ };
745
+ const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
746
+ const popupPromise = watchForNewPage
747
+ ? waitForPopup(page.context())
748
+ : Promise.resolve(null);
749
+ const tryRankedCandidates = async () => {
750
+ const resolution = await resolvePreparedLocatorCandidates({
751
+ target,
752
+ action,
753
+ baseRoot,
754
+ locatorRoot,
755
+ surfaceRoot,
756
+ attempts,
757
+ prepareOptions: {
758
+ allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
759
+ allowDescendantPressFallback: action === 'press' &&
760
+ (target.controlFamily === 'text-input' ||
761
+ target.controlFamily === 'select' ||
762
+ target.controlFamily === 'datepicker'),
763
+ isUserActionable: isLocatorUserActionable,
764
+ },
765
+ onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy),
677
766
  });
678
- if (preparedLocator.blockedReason === 'disabled') {
767
+ if (resolution.sawDisabledTarget) {
679
768
  sawDisabledTarget = true;
680
769
  }
681
- if (preparedLocator.blockedReason === 'readonly') {
770
+ if (resolution.sawReadonlyTarget) {
682
771
  sawReadonlyTarget = true;
683
772
  }
684
- const resolvedLocator = preparedLocator.locator;
685
- if (!resolvedLocator) {
686
- continue;
687
- }
688
- const resolved = await attemptResolvedLocator(resolvedLocator, candidate.strategy);
689
- if (resolved) {
690
- break;
773
+ };
774
+ await tryRankedCandidates();
775
+ if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
776
+ const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
777
+ if (deferredSurfaceRoot) {
778
+ surfaceRoot = deferredSurfaceRoot;
779
+ locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface) ? baseRoot : surfaceRoot;
780
+ await tryRankedCandidates();
691
781
  }
692
782
  }
693
- };
694
- await tryRankedCandidates();
695
- if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
696
- const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
697
- if (deferredSurfaceRoot) {
698
- surfaceRoot = deferredSurfaceRoot;
699
- locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface) ? baseRoot : surfaceRoot;
700
- await tryRankedCandidates();
701
- }
702
- }
703
- if (!resolvedBy && !lastError && surfaceRoot) {
704
- const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
705
- if (recoveredLocator) {
706
- await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
707
- skipDomSignature: true,
708
- });
709
- }
710
- }
711
- if (!resolvedBy) {
712
- if (sawDomSignatureMismatch) {
713
- staleReason = 'dom-signature-mismatch';
714
- throw new Error('stale_target_dom_signature_changed');
715
- }
716
- if (sawDisabledTarget) {
717
- setTargetAvailability(session, targetRef, 'gated', 'disabled');
718
- throw new Error('target_disabled');
719
- }
720
- if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
721
- setTargetAvailability(session, targetRef, 'gated', 'readonly');
722
- throw new Error('target_readonly');
723
- }
724
- if (!lastError &&
725
- target.surfaceRef &&
726
- (target.acceptancePolicy === 'selection' || target.acceptancePolicy === 'date-selection')) {
727
- setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
728
- throw new Error('target_surface_inactive');
783
+ if (!resolvedBy && !lastError && surfaceRoot) {
784
+ const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
785
+ if (recoveredLocator) {
786
+ await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
787
+ skipDomSignature: true,
788
+ });
789
+ }
729
790
  }
730
791
  if (!resolvedBy) {
792
+ if (sawDomSignatureMismatch) {
793
+ staleReason = 'dom-signature-mismatch';
794
+ throw new Error('stale_target_dom_signature_changed');
795
+ }
796
+ if (sawDisabledTarget) {
797
+ setTargetAvailability(session, targetRef, 'gated', 'disabled');
798
+ throw new Error('target_disabled');
799
+ }
800
+ if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
801
+ setTargetAvailability(session, targetRef, 'gated', 'readonly');
802
+ throw new Error('target_readonly');
803
+ }
731
804
  if (!lastError &&
732
- (target.controlFamily === 'text-input' ||
733
- target.controlFamily === 'select' ||
734
- target.controlFamily === 'datepicker')) {
735
- staleReason = 'locator-resolution-failed';
736
- throw new Error('stale_target_locator_resolution_failed');
805
+ target.surfaceRef &&
806
+ (target.acceptancePolicy === 'selection' || target.acceptancePolicy === 'date-selection')) {
807
+ setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
808
+ throw new Error('target_surface_inactive');
737
809
  }
738
- if (lastError) {
739
- throw lastError;
810
+ if (!resolvedBy) {
811
+ if (!lastError &&
812
+ (target.controlFamily === 'text-input' ||
813
+ target.controlFamily === 'select' ||
814
+ target.controlFamily === 'datepicker')) {
815
+ staleReason = 'locator-resolution-failed';
816
+ throw new Error('stale_target_locator_resolution_failed');
817
+ }
818
+ if (lastError) {
819
+ throw lastError;
820
+ }
821
+ throw new Error('deterministic_target_resolution_failed');
740
822
  }
741
- throw new Error('deterministic_target_resolution_failed');
742
823
  }
743
- }
744
- const popup = await popupPromise;
745
- const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
746
- if (latePage) {
747
- attempts.push('late-page-captured');
748
- }
749
- const discoveredPage = popup ?? latePage;
750
- const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
751
- const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
752
- let finalPageRef = target.pageRef;
753
- if (capturedPopup) {
754
- await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
755
- settleTimeoutMs: 1_500,
756
- });
757
- setCurrentPage(session, capturedPopup.page.pageRef);
758
- finalPageRef = capturedPopup.page.pageRef;
759
- currentPageRef = finalPageRef;
760
- }
761
- else {
762
- const syncedPage = await syncSessionPage(session, target.pageRef, page);
763
- currentPageRef = target.pageRef;
764
- if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
765
- clearProtectedExposure(session, target.pageRef);
824
+ const popup = await popupPromise;
825
+ const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
826
+ if (latePage) {
827
+ attempts.push('late-page-captured');
828
+ }
829
+ const discoveredPage = popup ?? latePage;
830
+ const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
831
+ const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
832
+ let finalPageRef = target.pageRef;
833
+ if (capturedPopup) {
834
+ await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
835
+ settleTimeoutMs: 1_500,
836
+ });
837
+ setCurrentPage(session, capturedPopup.page.pageRef);
838
+ finalPageRef = capturedPopup.page.pageRef;
839
+ currentPageRef = finalPageRef;
766
840
  }
767
- const finalProgressProbe = progressProbe;
768
- if (finalProgressProbe) {
769
- const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
770
- const afterPageObservation = acceptance.afterPageObservation;
771
- const accepted = acceptance.accepted;
772
- if (acceptance.polls > 1) {
773
- attempts.push(`acceptance.polled:${acceptance.polls}`);
841
+ else {
842
+ const syncedPage = await syncSessionPage(session, target.pageRef, page);
843
+ currentPageRef = target.pageRef;
844
+ if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
845
+ clearProtectedExposure(session, target.pageRef);
774
846
  }
775
- if (!accepted) {
776
- if (finalProgressProbe.policy === 'value-change') {
777
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
778
- throw new Error('action_postcondition_failed:value-change');
779
- }
780
- if ((finalProgressProbe.policy === 'selection' ||
781
- finalProgressProbe.policy === 'date-selection') &&
782
- finalProgressProbe.expectedValue !== null) {
783
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
784
- throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
847
+ const progressProbeForVerification = progressProbe;
848
+ if (progressProbeForVerification) {
849
+ const finalProgressProbe = progressProbeForVerification;
850
+ const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
851
+ const afterPageObservation = acceptance.afterPageObservation;
852
+ const accepted = acceptance.accepted;
853
+ if (acceptance.polls > 1) {
854
+ attempts.push(`acceptance.polled:${acceptance.polls}`);
785
855
  }
786
- if (finalProgressProbe.policy === 'submit') {
787
- const submitResolution = await resolveSubmitResult(finalProgressProbe, afterPageObservation);
788
- if (submitResolution.acceptAsProgress) {
789
- attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
790
- if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
791
- attempts.push('submit-resolution:soft-result-candidate');
856
+ if (!accepted) {
857
+ if (finalProgressProbe.policy === 'value-change') {
858
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
859
+ throw new Error('action_postcondition_failed:value-change');
860
+ }
861
+ if ((finalProgressProbe.policy === 'selection' ||
862
+ finalProgressProbe.policy === 'date-selection') &&
863
+ finalProgressProbe.expectedValue !== null) {
864
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
865
+ throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
866
+ }
867
+ if (finalProgressProbe.policy === 'submit') {
868
+ const submitResolution = await resolveSubmitResult(finalProgressProbe, afterPageObservation);
869
+ if (submitResolution.acceptAsProgress) {
870
+ attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
871
+ if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
872
+ attempts.push('submit-resolution:soft-result-candidate');
873
+ }
874
+ }
875
+ else {
876
+ attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
877
+ attempts.push('no-progress.detected');
878
+ noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
879
+ if (noProgressDiagnosis) {
880
+ attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
881
+ }
882
+ throw new Error('no_observable_progress');
792
883
  }
793
884
  }
794
885
  else {
795
- attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
796
- attempts.push('no-progress.detected');
797
- noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
798
- if (noProgressDiagnosis) {
799
- attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
886
+ const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
887
+ ? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
888
+ : null;
889
+ const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
890
+ const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
891
+ Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
892
+ Boolean(finalProgressProbe.beforePage || afterPageObservation);
893
+ const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
894
+ ? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
895
+ : pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
896
+ if (hasComparableSignal &&
897
+ !pageProgressChanged &&
898
+ finalProgressProbe.beforeContextHash === afterContextHash &&
899
+ !locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
900
+ attempts.push('no-progress.detected');
901
+ noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
902
+ if (noProgressDiagnosis) {
903
+ attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
904
+ }
905
+ throw new Error('no_observable_progress');
800
906
  }
801
- throw new Error('no_observable_progress');
802
907
  }
803
908
  }
804
909
  else {
805
- const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
806
- ? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
807
- : null;
808
- const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
809
- const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
810
- Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
811
- Boolean(finalProgressProbe.beforePage || afterPageObservation);
812
- const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
813
- ? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
814
- : pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
815
- if (hasComparableSignal &&
816
- !pageProgressChanged &&
817
- finalProgressProbe.beforeContextHash === afterContextHash &&
818
- !locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
819
- attempts.push('no-progress.detected');
820
- noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
821
- if (noProgressDiagnosis) {
822
- attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
823
- }
824
- throw new Error('no_observable_progress');
910
+ partialProgressResult = await partialProgressForAliasedSelection({
911
+ requestedAction,
912
+ probe: finalProgressProbe,
913
+ });
914
+ if (partialProgressResult) {
915
+ attempts.push('outcome.partial-progress:selection-not-complete');
825
916
  }
826
917
  }
827
918
  }
828
- }
829
- }
830
- if (resolvedBy === 'playwright-locator') {
831
- incrementMetric(session, 'deterministicActions');
832
- }
833
- bumpPageScopeEpoch(session, target.pageRef);
834
- recordActionResult(session, true, Date.now() - startedAt);
835
- saveSession(session);
836
- await trace.finishSuccess();
837
- outputJSON({
838
- success: true,
839
- targetRef,
840
- action,
841
- value: actionValue,
842
- resolvedBy,
843
- locatorStrategy,
844
- pageRef: finalPageRef,
845
- attempts,
846
- popup: Boolean(capturedPopup),
847
- overlayHandled: attempts.includes('overlay.dismissed'),
848
- iframe: Boolean(target.framePath?.length),
849
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
850
- ...(recoveredAfterError
851
- ? {
852
- recoveredAfterError: true,
853
- recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
919
+ else if (recoveredProgressProbe) {
920
+ partialProgressResult = await partialProgressForAliasedSelection({
921
+ requestedAction,
922
+ probe: recoveredProgressProbe,
923
+ });
924
+ if (partialProgressResult) {
925
+ attempts.push('outcome.partial-progress:selection-not-complete');
926
+ }
854
927
  }
855
- : {}),
856
- durationMs: Date.now() - startedAt,
857
- metrics: session.runtime?.metrics,
858
- });
859
- }
860
- catch (err) {
861
- failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
862
- recordActionResult(session, false, Date.now() - startedAt);
863
- if (staleReason) {
864
- markTargetLifecycle(session, targetRef, 'stale', staleReason);
928
+ }
929
+ if (resolvedBy === 'playwright-locator') {
930
+ incrementMetric(session, 'deterministicActions');
931
+ }
932
+ bumpPageScopeEpoch(session, target.pageRef);
933
+ recordActionResult(session, true, Date.now() - startedAt);
934
+ saveSession(session);
935
+ await trace.finishSuccess();
936
+ captureStepSnapshotBestEffort({
937
+ session,
938
+ step: actStep,
939
+ phase: 'after',
940
+ pageRef: finalPageRef,
941
+ });
942
+ appendCommandLifecycleEventBestEffort({
943
+ step: actStep,
944
+ phase: 'completed',
945
+ attributes: {
946
+ outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
947
+ targetRef,
948
+ action: requestedAction,
949
+ pageRef: finalPageRef,
950
+ },
951
+ });
952
+ finalizeActStepBestEffort(runId, actStep?.stepId, {
953
+ success: true,
954
+ outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
955
+ message: partialProgressResult?.message ?? 'The requested action completed successfully.',
956
+ });
957
+ await exportRunStepToOtlpHttpJsonBestEffort(runId, actStep?.stepId);
958
+ outputJSON({
959
+ success: true,
960
+ targetRef,
961
+ action: requestedAction,
962
+ ...(action !== requestedAction ? { executedAs: action } : {}),
963
+ value: actionValue,
964
+ resolvedBy,
965
+ locatorStrategy,
966
+ pageRef: finalPageRef,
967
+ attempts,
968
+ popup: Boolean(capturedPopup),
969
+ overlayHandled: attempts.includes('overlay.dismissed'),
970
+ iframe: Boolean(target.framePath?.length),
971
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
972
+ ...(recoveredAfterError
973
+ ? {
974
+ recoveredAfterError: true,
975
+ recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
976
+ }
977
+ : {}),
978
+ ...(partialProgressResult ?? {}),
979
+ durationMs: Date.now() - startedAt,
980
+ metrics: session.runtime?.metrics,
981
+ });
865
982
  }
866
- saveSession(session);
867
- if (currentPage) {
868
- try {
869
- const protectedExposure = getProtectedExposure(session, currentPageRef);
870
- if (protectedExposure) {
871
- await trace.finishSuccess();
872
- failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
983
+ catch (err) {
984
+ failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
985
+ recordActionResult(session, false, Date.now() - startedAt);
986
+ if (staleReason) {
987
+ markTargetLifecycle(session, targetRef, 'stale', staleReason);
988
+ }
989
+ saveSession(session);
990
+ if (currentPage) {
991
+ try {
992
+ const protectedExposure = getProtectedExposure(session, currentPageRef);
993
+ if (protectedExposure) {
994
+ await trace.finishSuccess();
995
+ failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
996
+ }
997
+ else {
998
+ failureArtifacts = await captureActionFailureArtifacts({
999
+ page: currentPage,
1000
+ targetRef,
1001
+ action: requestedAction,
1002
+ pageRef: currentPageRef,
1003
+ attempts,
1004
+ locatorStrategy,
1005
+ popup: attempts.includes('popup-captured'),
1006
+ overlayHandled: attempts.includes('overlay.dismissed'),
1007
+ iframe: Boolean(target.framePath?.length),
1008
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1009
+ durationMs: Date.now() - startedAt,
1010
+ error: failureMessage,
1011
+ finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
1012
+ });
1013
+ }
873
1014
  }
874
- else {
875
- failureArtifacts = await captureActionFailureArtifacts({
876
- page: currentPage,
877
- targetRef,
878
- action,
879
- pageRef: currentPageRef,
880
- attempts,
881
- locatorStrategy,
882
- popup: attempts.includes('popup-captured'),
883
- overlayHandled: attempts.includes('overlay.dismissed'),
884
- iframe: Boolean(target.framePath?.length),
885
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
886
- durationMs: Date.now() - startedAt,
887
- error: failureMessage,
888
- finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
889
- });
1015
+ catch {
1016
+ // Best effort only. Preserve the original action failure.
890
1017
  }
891
1018
  }
892
- catch {
893
- // Best effort only. Preserve the original action failure.
894
- }
895
1019
  }
896
- }
897
- finally {
898
- if (browser) {
899
- disconnectPlaywright(browser);
1020
+ finally {
1021
+ if (browser) {
1022
+ await disconnectPlaywright(browser);
1023
+ }
900
1024
  }
901
- }
902
- if (failureMessage) {
903
- if (failureMessage === 'Act failed: no_observable_progress' && noProgressDiagnosis) {
904
- failureMessage = `Act failed: no_observable_progress (${noProgressDiagnosis.kind})`;
1025
+ if (failureMessage) {
1026
+ if (failureMessage === 'Act failed: no_observable_progress' && noProgressDiagnosis) {
1027
+ failureMessage = `Act failed: no_observable_progress (${noProgressDiagnosis.kind})`;
1028
+ }
1029
+ const failureContract = describeActFailure({
1030
+ failureMessage,
1031
+ staleReason,
1032
+ diagnosis: noProgressDiagnosis,
1033
+ });
1034
+ const protectedExposure = getProtectedExposure(session, currentPageRef);
1035
+ const outputDiagnosis = protectedExposure && noProgressDiagnosis
1036
+ ? redactNoObservableProgressDiagnosis(noProgressDiagnosis)
1037
+ : noProgressDiagnosis;
1038
+ const artifactManifestId = persistActArtifactManifestBestEffort(runId, actStep?.stepId, failureArtifacts ?? null);
1039
+ captureStepSnapshotBestEffort({
1040
+ session,
1041
+ step: actStep,
1042
+ phase: 'point-in-time',
1043
+ pageRef: currentPageRef,
1044
+ artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
1045
+ });
1046
+ appendCommandLifecycleEventBestEffort({
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
+ finalizeActStepBestEffort(runId, actStep?.stepId, {
1058
+ success: false,
1059
+ outcomeType: failureContract.outcomeType,
1060
+ message: failureContract.message,
1061
+ reason: failureContract.reason,
1062
+ artifactManifestId,
1063
+ });
1064
+ await exportRunStepToOtlpHttpJsonBestEffort(runId, actStep?.stepId);
1065
+ outputFailure({
1066
+ error: failureMessage,
1067
+ outcomeType: failureContract.outcomeType,
1068
+ message: failureContract.message,
1069
+ reason: failureContract.reason,
1070
+ targetRef,
1071
+ action: requestedAction,
1072
+ ...(action !== requestedAction ? { executedAs: action } : {}),
1073
+ value: actionValue,
1074
+ pageRef: currentPageRef,
1075
+ locatorStrategy,
1076
+ attempts,
1077
+ popup: attempts.includes('popup-captured'),
1078
+ overlayHandled: attempts.includes('overlay.dismissed'),
1079
+ iframe: Boolean(target.framePath?.length),
1080
+ jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
1081
+ durationMs: Date.now() - startedAt,
1082
+ staleTarget: Boolean(staleReason),
1083
+ staleReason: staleReason ?? undefined,
1084
+ diagnosis: outputDiagnosis ?? undefined,
1085
+ artifacts: failureArtifacts,
1086
+ metrics: session.runtime?.metrics,
1087
+ });
905
1088
  }
906
- const failureContract = describeActFailure({
907
- failureMessage,
908
- staleReason,
909
- diagnosis: noProgressDiagnosis,
910
- });
911
- const protectedExposure = getProtectedExposure(session, currentPageRef);
912
- const outputDiagnosis = protectedExposure && noProgressDiagnosis
913
- ? redactNoObservableProgressDiagnosis(noProgressDiagnosis)
914
- : noProgressDiagnosis;
915
- outputFailure({
916
- error: failureMessage,
917
- outcomeType: failureContract.outcomeType,
918
- message: failureContract.message,
919
- reason: failureContract.reason,
920
- targetRef,
921
- action,
922
- value: actionValue,
923
- pageRef: currentPageRef,
924
- locatorStrategy,
925
- attempts,
926
- popup: attempts.includes('popup-captured'),
927
- overlayHandled: attempts.includes('overlay.dismissed'),
928
- iframe: Boolean(target.framePath?.length),
929
- jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
930
- durationMs: Date.now() - startedAt,
931
- staleTarget: Boolean(staleReason),
932
- staleReason: staleReason ?? undefined,
933
- diagnosis: outputDiagnosis ?? undefined,
934
- artifacts: failureArtifacts,
935
- metrics: session.runtime?.metrics,
936
- });
937
- }
1089
+ });
938
1090
  }