browser-debug-mcp-bridge 1.11.1 → 1.12.0

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.
@@ -5,6 +5,7 @@ import { createHash, randomUUID } from 'crypto';
5
5
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
6
6
  import { dirname, resolve } from 'path';
7
7
  import { z } from 'zod';
8
+ import { WorkflowTargetResolutionError, hasSemanticActionTargetMatcher, resolveWorkflowActionTarget, summarizeWorkflowTargetMatcher, } from './target-resolution.js';
8
9
  import { getConnection } from '../db/connection.js';
9
10
  import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
10
11
  import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
@@ -15,6 +16,7 @@ import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
15
16
  import { planNextSourceOverride } from '../next-source-override-planner.js';
16
17
  import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
17
18
  import { planOverrideResponsePatch } from '../override-response-planner.js';
19
+ import { createToolLoopGuard } from './tool-loop-guard.js';
18
20
  function createDefaultMcpLogger() {
19
21
  const write = (level, message, payload) => {
20
22
  process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
@@ -31,12 +33,93 @@ function createDefaultMcpLogger() {
31
33
  },
32
34
  };
33
35
  }
36
+ const UIActionTargetScopeSchema = z.enum(['buttons', 'links', 'inputs', 'modals', 'focused']);
37
+ const UIActionLocatorMatcherSchema = z.union([
38
+ z.string().min(1),
39
+ z.object({
40
+ pattern: z.string().min(1),
41
+ flags: z.string().regex(/^[imsu]*$/).optional(),
42
+ }),
43
+ ]);
44
+ const UIActionLocatorStepSchema = z.object({
45
+ kind: z.enum(['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText']),
46
+ value: UIActionLocatorMatcherSchema.optional(),
47
+ role: z.string().min(1).optional(),
48
+ name: UIActionLocatorMatcherSchema.optional(),
49
+ exact: z.boolean().optional(),
50
+ relation: z.enum(['filter', 'descendant', 'ancestor']).optional(),
51
+ }).superRefine((value, ctx) => {
52
+ if (value.kind === 'role' && !value.role && !value.value) {
53
+ ctx.addIssue({
54
+ code: z.ZodIssueCode.custom,
55
+ message: 'role locator step requires role or value',
56
+ path: ['role'],
57
+ });
58
+ }
59
+ if (value.kind !== 'role' && !value.value) {
60
+ ctx.addIssue({
61
+ code: z.ZodIssueCode.custom,
62
+ message: `${value.kind} locator step requires value`,
63
+ path: ['value'],
64
+ });
65
+ }
66
+ });
67
+ const UIActionLocatorSchema = z.object({
68
+ scope: UIActionTargetScopeSchema.optional(),
69
+ frame: z.object({
70
+ selector: z.string().min(1).optional(),
71
+ urlContains: z.string().min(1).optional(),
72
+ titleContains: z.string().min(1).optional(),
73
+ }).optional(),
74
+ steps: z.array(UIActionLocatorStepSchema).min(1).max(8),
75
+ });
76
+ const UIActionCoordinateTargetSchema = z.object({
77
+ x: z.number().finite(),
78
+ y: z.number().finite(),
79
+ frameId: z.number().int().min(0).optional(),
80
+ });
34
81
  const LiveUIActionTargetSchema = z.object({
35
82
  selector: z.string().min(1).optional(),
36
83
  elementRef: z.string().min(1).optional(),
84
+ coordinates: UIActionCoordinateTargetSchema.optional(),
37
85
  tabId: z.number().int().min(0).optional(),
38
86
  frameId: z.number().int().min(0).optional(),
39
87
  url: z.string().url().optional(),
88
+ locator: UIActionLocatorSchema.optional(),
89
+ frameUrlContains: z.string().min(1).optional(),
90
+ frameTitleContains: z.string().min(1).optional(),
91
+ testId: z.string().min(1).optional(),
92
+ scope: UIActionTargetScopeSchema.optional(),
93
+ textContains: z.string().min(1).optional(),
94
+ labelContains: z.string().min(1).optional(),
95
+ titleContains: z.string().min(1).optional(),
96
+ role: z.string().min(1).optional(),
97
+ name: z.string().min(1).optional(),
98
+ placeholder: z.string().min(1).optional(),
99
+ altText: z.string().min(1).optional(),
100
+ tagName: z.string().min(1).optional(),
101
+ type: z.string().min(1).optional(),
102
+ exact: z.boolean().optional(),
103
+ nth: z.number().int().min(0).optional(),
104
+ first: z.boolean().optional(),
105
+ last: z.boolean().optional(),
106
+ strict: z.boolean().optional(),
107
+ visible: z.boolean().optional(),
108
+ disabled: z.boolean().optional(),
109
+ selected: z.boolean().optional(),
110
+ pressed: z.boolean().optional(),
111
+ expanded: z.boolean().optional(),
112
+ readOnly: z.boolean().optional(),
113
+ requiredField: z.boolean().optional(),
114
+ }).superRefine((value, ctx) => {
115
+ const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
116
+ if (positionFields > 1) {
117
+ ctx.addIssue({
118
+ code: z.ZodIssueCode.custom,
119
+ message: 'target can use only one of nth, first, or last',
120
+ path: ['target'],
121
+ });
122
+ }
40
123
  });
41
124
  const LiveUIActionBaseSchema = z.object({
42
125
  traceId: z.string().min(1).optional(),
@@ -50,6 +133,10 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
50
133
  clickCount: z.number().int().min(1).max(3).optional(),
51
134
  }).optional(),
52
135
  }),
136
+ LiveUIActionBaseSchema.extend({
137
+ action: z.literal('hover'),
138
+ input: z.object({}).optional(),
139
+ }),
53
140
  LiveUIActionBaseSchema.extend({
54
141
  action: z.literal('input'),
55
142
  input: z.object({
@@ -95,20 +182,34 @@ const LiveUIActionRequestSchema = z.discriminatedUnion('action', [
95
182
  ]);
96
183
  const UIWorkflowModeSchema = z.enum(['safe', 'fast']);
97
184
  const UIWorkflowFailureStrategySchema = z.enum(['stop', 'continue', 'retry_once']);
98
- const UIWorkflowActionTargetScopeSchema = z.enum(['buttons', 'inputs', 'modals', 'focused']);
185
+ const UIWorkflowActionTargetScopeSchema = UIActionTargetScopeSchema;
99
186
  const UIWorkflowActionTargetSchema = z.object({
100
187
  selector: z.string().min(1).optional(),
101
188
  elementRef: z.string().min(1).optional(),
189
+ coordinates: UIActionCoordinateTargetSchema.optional(),
102
190
  tabId: z.number().int().min(0).optional(),
103
191
  frameId: z.number().int().min(0).optional(),
104
192
  url: z.string().url().optional(),
193
+ locator: UIActionLocatorSchema.optional(),
194
+ frameUrlContains: z.string().min(1).optional(),
195
+ frameTitleContains: z.string().min(1).optional(),
105
196
  testId: z.string().min(1).optional(),
106
197
  scope: UIWorkflowActionTargetScopeSchema.optional(),
107
198
  textContains: z.string().min(1).optional(),
108
199
  labelContains: z.string().min(1).optional(),
109
200
  titleContains: z.string().min(1).optional(),
201
+ role: z.string().min(1).optional(),
202
+ name: z.string().min(1).optional(),
203
+ placeholder: z.string().min(1).optional(),
204
+ altText: z.string().min(1).optional(),
110
205
  tagName: z.string().min(1).optional(),
111
206
  type: z.string().min(1).optional(),
207
+ exact: z.boolean().optional(),
208
+ nth: z.number().int().min(0).optional(),
209
+ first: z.boolean().optional(),
210
+ last: z.boolean().optional(),
211
+ strict: z.boolean().optional(),
212
+ visible: z.boolean().optional(),
112
213
  disabled: z.boolean().optional(),
113
214
  selected: z.boolean().optional(),
114
215
  pressed: z.boolean().optional(),
@@ -118,13 +219,28 @@ const UIWorkflowActionTargetSchema = z.object({
118
219
  }).superRefine((value, ctx) => {
119
220
  if (!value.selector
120
221
  && !value.elementRef
222
+ && !value.coordinates
223
+ && !value.locator
224
+ && !value.scope
121
225
  && !value.testId
122
226
  && !value.textContains
123
227
  && !value.labelContains
124
- && !value.titleContains) {
228
+ && !value.titleContains
229
+ && !value.role
230
+ && !value.name
231
+ && !value.placeholder
232
+ && !value.altText) {
233
+ ctx.addIssue({
234
+ code: z.ZodIssueCode.custom,
235
+ message: 'target requires selector, elementRef, coordinates, locator, scope, testId, textContains, labelContains, titleContains, role, name, placeholder, or altText',
236
+ path: ['target'],
237
+ });
238
+ }
239
+ const positionFields = [value.nth !== undefined, value.first === true, value.last === true].filter(Boolean).length;
240
+ if (positionFields > 1) {
125
241
  ctx.addIssue({
126
242
  code: z.ZodIssueCode.custom,
127
- message: 'target requires selector, elementRef, testId, textContains, labelContains, or titleContains',
243
+ message: 'target can use only one of nth, first, or last',
128
244
  path: ['target'],
129
245
  });
130
246
  }
@@ -163,6 +279,10 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
163
279
  clickCount: z.number().int().min(1).max(3).optional(),
164
280
  }).optional(),
165
281
  }),
282
+ UIWorkflowActionBaseSchema.extend({
283
+ action: z.literal('hover'),
284
+ input: z.object({}).optional(),
285
+ }),
166
286
  UIWorkflowActionBaseSchema.extend({
167
287
  action: z.literal('input'),
168
288
  input: z.object({
@@ -207,14 +327,22 @@ const UIWorkflowActionStepSchema = z.discriminatedUnion('action', [
207
327
  }),
208
328
  ]);
209
329
  const UIWorkflowPageStateMatcherSchema = z.object({
210
- scope: z.enum(['buttons', 'inputs', 'modals', 'focused', 'page']),
330
+ scope: z.enum(['buttons', 'links', 'inputs', 'modals', 'focused', 'page']),
211
331
  selector: z.string().optional(),
212
332
  testId: z.string().optional(),
213
333
  textContains: z.string().optional(),
214
334
  labelContains: z.string().optional(),
215
335
  titleContains: z.string().optional(),
336
+ role: z.string().optional(),
337
+ name: z.string().optional(),
338
+ placeholder: z.string().optional(),
339
+ altText: z.string().optional(),
340
+ exact: z.boolean().optional(),
341
+ frameUrlContains: z.string().optional(),
342
+ frameTitleContains: z.string().optional(),
216
343
  urlContains: z.string().optional(),
217
344
  language: z.string().optional(),
345
+ visible: z.boolean().optional(),
218
346
  disabled: z.boolean().optional(),
219
347
  selected: z.boolean().optional(),
220
348
  pressed: z.boolean().optional(),
@@ -247,10 +375,201 @@ const UIWorkflowAssertStepSchema = UIWorkflowStepBaseSchema.extend({
247
375
  kind: z.literal('assert'),
248
376
  matcher: UIWorkflowPageStateMatcherSchema,
249
377
  });
378
+ const AutomationWaitBaseSchema = z.object({
379
+ timeoutMs: z.number().int().min(100).max(120000).optional(),
380
+ pollIntervalMs: z.number().int().min(50).max(5000).optional(),
381
+ });
382
+ const AutomationWaitUrlSchema = AutomationWaitBaseSchema.extend({
383
+ waitKind: z.literal('url'),
384
+ urlContains: z.string().min(1).optional(),
385
+ urlRegex: z.string().min(1).optional(),
386
+ exactUrl: z.string().min(1).optional(),
387
+ }).superRefine((value, ctx) => {
388
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl) {
389
+ ctx.addIssue({
390
+ code: z.ZodIssueCode.custom,
391
+ message: 'url wait requires urlContains, urlRegex, or exactUrl',
392
+ path: ['wait'],
393
+ });
394
+ }
395
+ });
396
+ const AutomationWaitNavigationSchema = AutomationWaitBaseSchema.extend({
397
+ waitKind: z.literal('navigation'),
398
+ urlContains: z.string().min(1).optional(),
399
+ urlRegex: z.string().min(1).optional(),
400
+ exactUrl: z.string().min(1).optional(),
401
+ fromUrlContains: z.string().min(1).optional(),
402
+ fromUrlRegex: z.string().min(1).optional(),
403
+ trigger: z.string().min(1).optional(),
404
+ sinceTs: z.number().int().min(0).optional(),
405
+ tabId: z.number().int().min(0).optional(),
406
+ }).superRefine((value, ctx) => {
407
+ if (!value.urlContains
408
+ && !value.urlRegex
409
+ && !value.exactUrl
410
+ && !value.fromUrlContains
411
+ && !value.fromUrlRegex
412
+ && !value.trigger) {
413
+ ctx.addIssue({
414
+ code: z.ZodIssueCode.custom,
415
+ message: 'navigation wait requires a URL, from-URL, or trigger predicate',
416
+ path: ['wait'],
417
+ });
418
+ }
419
+ });
420
+ const AutomationWaitNavigationLifecycleSchema = AutomationWaitBaseSchema.extend({
421
+ waitKind: z.literal('navigation_lifecycle'),
422
+ state: z.enum(['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle']).default('load'),
423
+ urlContains: z.string().min(1).optional(),
424
+ urlRegex: z.string().min(1).optional(),
425
+ exactUrl: z.string().min(1).optional(),
426
+ tabId: z.number().int().min(0).optional(),
427
+ });
428
+ const AutomationWaitLoadStateSchema = AutomationWaitBaseSchema.extend({
429
+ waitKind: z.literal('load_state'),
430
+ state: z.enum(['domcontentloaded', 'load']).default('load'),
431
+ urlContains: z.string().min(1).optional(),
432
+ urlRegex: z.string().min(1).optional(),
433
+ exactUrl: z.string().min(1).optional(),
434
+ });
435
+ const AutomationWaitSelectorStateSchema = AutomationWaitBaseSchema.extend({
436
+ waitKind: z.literal('selector_state'),
437
+ selector: z.string().min(1),
438
+ state: z.enum(['attached', 'detached', 'visible', 'hidden']).default('visible'),
439
+ frameId: z.number().int().min(0).default(0),
440
+ });
441
+ const AutomationWaitConsoleSchema = AutomationWaitBaseSchema.extend({
442
+ waitKind: z.literal('console'),
443
+ levels: z.array(z.string().min(1)).optional(),
444
+ contains: z.string().min(1).optional(),
445
+ sinceTs: z.number().int().min(0).optional(),
446
+ includeRuntimeErrors: z.boolean().optional(),
447
+ });
448
+ const AutomationWaitDialogSchema = AutomationWaitBaseSchema.extend({
449
+ waitKind: z.literal('dialog'),
450
+ type: z.enum(['alert', 'confirm', 'prompt', 'beforeunload']).optional(),
451
+ messageContains: z.string().min(1).optional(),
452
+ urlContains: z.string().min(1).optional(),
453
+ action: z.enum(['none', 'accept', 'dismiss']).default('none'),
454
+ promptText: z.string().optional(),
455
+ tabId: z.number().int().min(0).optional(),
456
+ });
457
+ const AutomationWaitStableLayoutSchema = AutomationWaitBaseSchema.extend({
458
+ waitKind: z.literal('stable_layout'),
459
+ selector: z.string().min(1).optional(),
460
+ stableMs: z.number().int().min(100).max(10000).default(500),
461
+ tabId: z.number().int().min(0).optional(),
462
+ });
463
+ const AutomationWaitDownloadSchema = AutomationWaitBaseSchema.extend({
464
+ waitKind: z.literal('download'),
465
+ urlContains: z.string().min(1).optional(),
466
+ urlRegex: z.string().min(1).optional(),
467
+ exactUrl: z.string().min(1).optional(),
468
+ filenameContains: z.string().min(1).optional(),
469
+ filenameRegex: z.string().min(1).optional(),
470
+ state: z.enum(['started', 'completed']).default('started'),
471
+ tabId: z.number().int().min(0).optional(),
472
+ }).superRefine((value, ctx) => {
473
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.filenameContains && !value.filenameRegex) {
474
+ ctx.addIssue({
475
+ code: z.ZodIssueCode.custom,
476
+ message: 'download wait requires a URL or filename predicate',
477
+ path: ['wait'],
478
+ });
479
+ }
480
+ });
481
+ const AutomationWaitPopupSchema = AutomationWaitBaseSchema.extend({
482
+ waitKind: z.literal('popup'),
483
+ urlContains: z.string().min(1).optional(),
484
+ urlRegex: z.string().min(1).optional(),
485
+ exactUrl: z.string().min(1).optional(),
486
+ openerTabId: z.number().int().min(0).optional(),
487
+ }).superRefine((value, ctx) => {
488
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && value.openerTabId === undefined) {
489
+ ctx.addIssue({
490
+ code: z.ZodIssueCode.custom,
491
+ message: 'popup wait requires a URL predicate or openerTabId',
492
+ path: ['wait'],
493
+ });
494
+ }
495
+ });
496
+ const AutomationWaitNetworkQuietSchema = AutomationWaitBaseSchema.extend({
497
+ waitKind: z.literal('network_quiet'),
498
+ quietMs: z.number().int().min(100).max(10000).default(500),
499
+ urlContains: z.string().min(1).optional(),
500
+ method: z.string().min(1).optional(),
501
+ tabId: z.number().int().min(0).optional(),
502
+ });
503
+ const AutomationWaitNetworkBaseSchema = AutomationWaitBaseSchema.extend({
504
+ urlContains: z.string().min(1).optional(),
505
+ urlRegex: z.string().min(1).optional(),
506
+ exactUrl: z.string().min(1).optional(),
507
+ method: z.string().min(1).optional(),
508
+ traceId: z.string().min(1).optional(),
509
+ initiator: z.enum(['fetch', 'xhr', 'img', 'script', 'other']).optional(),
510
+ requestContentType: z.string().min(1).optional(),
511
+ sinceTs: z.number().int().min(0).optional(),
512
+ tabId: z.number().int().min(0).optional(),
513
+ includeBodies: z.boolean().optional(),
514
+ });
515
+ const AutomationWaitRequestSchema = AutomationWaitNetworkBaseSchema.extend({
516
+ waitKind: z.literal('request'),
517
+ }).superRefine((value, ctx) => {
518
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
519
+ ctx.addIssue({
520
+ code: z.ZodIssueCode.custom,
521
+ message: 'request wait requires urlContains, urlRegex, exactUrl, or traceId',
522
+ path: ['wait'],
523
+ });
524
+ }
525
+ });
526
+ const AutomationWaitResponseSchema = AutomationWaitNetworkBaseSchema.extend({
527
+ waitKind: z.literal('response'),
528
+ statusIn: z.array(z.number().int().min(100).max(599)).optional(),
529
+ statusGte: z.number().int().min(100).max(599).optional(),
530
+ statusLt: z.number().int().min(100).max(600).optional(),
531
+ responseContentType: z.string().min(1).optional(),
532
+ errorType: z.string().min(1).optional(),
533
+ }).superRefine((value, ctx) => {
534
+ if (!value.urlContains && !value.urlRegex && !value.exactUrl && !value.traceId) {
535
+ ctx.addIssue({
536
+ code: z.ZodIssueCode.custom,
537
+ message: 'response wait requires urlContains, urlRegex, exactUrl, or traceId',
538
+ path: ['wait'],
539
+ });
540
+ }
541
+ if (value.statusGte !== undefined && value.statusLt !== undefined && value.statusGte >= value.statusLt) {
542
+ ctx.addIssue({
543
+ code: z.ZodIssueCode.custom,
544
+ message: 'statusGte must be less than statusLt',
545
+ path: ['statusGte'],
546
+ });
547
+ }
548
+ });
549
+ const AutomationWaitSpecSchema = z.discriminatedUnion('waitKind', [
550
+ AutomationWaitUrlSchema,
551
+ AutomationWaitNavigationSchema,
552
+ AutomationWaitNavigationLifecycleSchema,
553
+ AutomationWaitLoadStateSchema,
554
+ AutomationWaitSelectorStateSchema,
555
+ AutomationWaitConsoleSchema,
556
+ AutomationWaitDialogSchema,
557
+ AutomationWaitStableLayoutSchema,
558
+ AutomationWaitDownloadSchema,
559
+ AutomationWaitPopupSchema,
560
+ AutomationWaitNetworkQuietSchema,
561
+ AutomationWaitRequestSchema,
562
+ AutomationWaitResponseSchema,
563
+ ]);
564
+ const UIWorkflowGenericWaitStepSchema = UIWorkflowStepBaseSchema.extend({
565
+ kind: z.literal('wait'),
566
+ wait: AutomationWaitSpecSchema,
567
+ });
250
568
  const UIWorkflowStepSchema = z.discriminatedUnion('kind', [
251
569
  UIWorkflowActionStepSchema,
252
570
  UIWorkflowWaitForStepSchema,
253
571
  UIWorkflowAssertStepSchema,
572
+ UIWorkflowGenericWaitStepSchema,
254
573
  ]);
255
574
  const RunUIStepsSchema = z.object({
256
575
  sessionId: z.string().min(1),
@@ -263,6 +582,91 @@ const RunUIStepsSchema = z.object({
263
582
  function createUIWorkflowTraceId() {
264
583
  return `uiworkflow-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
265
584
  }
585
+ const LOCATOR_MATCHER_TOOL_SCHEMA = {
586
+ anyOf: [
587
+ { type: 'string' },
588
+ {
589
+ type: 'object',
590
+ required: ['pattern'],
591
+ properties: {
592
+ pattern: { type: 'string' },
593
+ flags: { type: 'string' },
594
+ },
595
+ },
596
+ ],
597
+ };
598
+ const ACTION_LOCATOR_TOOL_SCHEMA = {
599
+ type: 'object',
600
+ required: ['steps'],
601
+ properties: {
602
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
603
+ frame: {
604
+ type: 'object',
605
+ properties: {
606
+ selector: { type: 'string' },
607
+ urlContains: { type: 'string' },
608
+ titleContains: { type: 'string' },
609
+ },
610
+ },
611
+ steps: {
612
+ type: 'array',
613
+ minItems: 1,
614
+ maxItems: 8,
615
+ items: {
616
+ type: 'object',
617
+ required: ['kind'],
618
+ properties: {
619
+ kind: { type: 'string', enum: ['css', 'role', 'text', 'label', 'testId', 'placeholder', 'altText'] },
620
+ value: LOCATOR_MATCHER_TOOL_SCHEMA,
621
+ role: { type: 'string' },
622
+ name: LOCATOR_MATCHER_TOOL_SCHEMA,
623
+ exact: { type: 'boolean' },
624
+ relation: { type: 'string', enum: ['filter', 'descendant', 'ancestor'] },
625
+ },
626
+ },
627
+ },
628
+ },
629
+ };
630
+ const AUTOMATION_WAIT_TOOL_SCHEMA = {
631
+ type: 'object',
632
+ required: ['waitKind'],
633
+ properties: {
634
+ waitKind: { type: 'string', enum: ['url', 'navigation', 'navigation_lifecycle', 'load_state', 'selector_state', 'console', 'dialog', 'stable_layout', 'download', 'popup', 'network_quiet', 'request', 'response'] },
635
+ timeoutMs: { type: 'number' },
636
+ pollIntervalMs: { type: 'number' },
637
+ urlContains: { type: 'string' },
638
+ urlRegex: { type: 'string' },
639
+ exactUrl: { type: 'string' },
640
+ fromUrlContains: { type: 'string' },
641
+ fromUrlRegex: { type: 'string' },
642
+ trigger: { type: 'string' },
643
+ state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle', 'attached', 'detached', 'visible', 'hidden', 'started', 'completed'] },
644
+ selector: { type: 'string' },
645
+ frameId: { type: 'number' },
646
+ levels: { type: 'array', items: { type: 'string' } },
647
+ contains: { type: 'string' },
648
+ sinceTs: { type: 'number' },
649
+ includeRuntimeErrors: { type: 'boolean' },
650
+ action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
651
+ promptText: { type: 'string' },
652
+ stableMs: { type: 'number' },
653
+ filenameContains: { type: 'string' },
654
+ filenameRegex: { type: 'string' },
655
+ openerTabId: { type: 'number' },
656
+ quietMs: { type: 'number' },
657
+ method: { type: 'string' },
658
+ traceId: { type: 'string' },
659
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
660
+ requestContentType: { type: 'string' },
661
+ responseContentType: { type: 'string' },
662
+ statusIn: { type: 'array', items: { type: 'number' } },
663
+ statusGte: { type: 'number' },
664
+ statusLt: { type: 'number' },
665
+ errorType: { type: 'string' },
666
+ includeBodies: { type: 'boolean' },
667
+ tabId: { type: 'number' },
668
+ },
669
+ };
266
670
  const TOOL_SCHEMAS = {
267
671
  list_sessions: {
268
672
  type: 'object',
@@ -451,6 +855,7 @@ const TOOL_SCHEMAS = {
451
855
  properties: {
452
856
  sessionId: { type: 'string' },
453
857
  selector: { type: 'string' },
858
+ frameId: { type: 'number' },
454
859
  properties: { type: 'array', items: { type: 'string' } },
455
860
  },
456
861
  },
@@ -460,6 +865,7 @@ const TOOL_SCHEMAS = {
460
865
  properties: {
461
866
  sessionId: { type: 'string' },
462
867
  selector: { type: 'string' },
868
+ frameId: { type: 'number' },
463
869
  },
464
870
  },
465
871
  get_page_state: {
@@ -470,6 +876,7 @@ const TOOL_SCHEMAS = {
470
876
  maxItems: { type: 'number' },
471
877
  maxTextLength: { type: 'number' },
472
878
  includeButtons: { type: 'boolean' },
879
+ includeLinks: { type: 'boolean' },
473
880
  includeInputs: { type: 'boolean' },
474
881
  includeModals: { type: 'boolean' },
475
882
  },
@@ -481,7 +888,7 @@ const TOOL_SCHEMAS = {
481
888
  sessionId: { type: 'string' },
482
889
  kinds: {
483
890
  type: 'array',
484
- items: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
891
+ items: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
485
892
  },
486
893
  maxItems: { type: 'number' },
487
894
  maxTextLength: { type: 'number' },
@@ -501,14 +908,22 @@ const TOOL_SCHEMAS = {
501
908
  required: ['sessionId', 'scope'],
502
909
  properties: {
503
910
  sessionId: { type: 'string' },
504
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
911
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
505
912
  selector: { type: 'string' },
506
913
  testId: { type: 'string' },
507
914
  textContains: { type: 'string' },
508
915
  labelContains: { type: 'string' },
509
916
  titleContains: { type: 'string' },
917
+ role: { type: 'string' },
918
+ name: { type: 'string' },
919
+ placeholder: { type: 'string' },
920
+ altText: { type: 'string' },
921
+ exact: { type: 'boolean' },
922
+ frameUrlContains: { type: 'string' },
923
+ frameTitleContains: { type: 'string' },
510
924
  urlContains: { type: 'string' },
511
925
  language: { type: 'string' },
926
+ visible: { type: 'boolean' },
512
927
  disabled: { type: 'boolean' },
513
928
  selected: { type: 'boolean' },
514
929
  pressed: { type: 'boolean' },
@@ -528,14 +943,22 @@ const TOOL_SCHEMAS = {
528
943
  required: ['sessionId', 'scope'],
529
944
  properties: {
530
945
  sessionId: { type: 'string' },
531
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
946
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
532
947
  selector: { type: 'string' },
533
948
  testId: { type: 'string' },
534
949
  textContains: { type: 'string' },
535
950
  labelContains: { type: 'string' },
536
951
  titleContains: { type: 'string' },
952
+ role: { type: 'string' },
953
+ name: { type: 'string' },
954
+ placeholder: { type: 'string' },
955
+ altText: { type: 'string' },
956
+ exact: { type: 'boolean' },
957
+ frameUrlContains: { type: 'string' },
958
+ frameTitleContains: { type: 'string' },
537
959
  urlContains: { type: 'string' },
538
960
  language: { type: 'string' },
961
+ visible: { type: 'boolean' },
539
962
  disabled: { type: 'boolean' },
540
963
  selected: { type: 'boolean' },
541
964
  pressed: { type: 'boolean' },
@@ -552,140 +975,352 @@ const TOOL_SCHEMAS = {
552
975
  pollIntervalMs: { type: 'number' },
553
976
  },
554
977
  },
555
- capture_ui_snapshot: {
978
+ preflight_automation_flow: {
556
979
  type: 'object',
557
980
  required: ['sessionId'],
558
981
  properties: {
559
982
  sessionId: { type: 'string' },
560
- selector: { type: 'string' },
561
- trigger: { type: 'string' },
562
- mode: { type: 'string' },
563
- styleMode: { type: 'string' },
564
- maxDepth: { type: 'number' },
565
- maxBytes: { type: 'number' },
566
- maxAncestors: { type: 'number' },
567
- includeDom: { type: 'boolean' },
568
- includeStyles: { type: 'boolean' },
569
- includePngDataUrl: { type: 'boolean' },
983
+ expectedUrlContains: { type: 'string' },
984
+ requireSensitiveAutomation: { type: 'boolean' },
985
+ plannedActions: { type: 'array', items: { type: 'string' } },
986
+ includePageState: { type: 'boolean' },
987
+ maxItems: { type: 'number' },
988
+ maxTextLength: { type: 'number' },
570
989
  },
571
990
  },
572
- get_live_console_logs: {
991
+ wait_for_url: {
573
992
  type: 'object',
574
993
  required: ['sessionId'],
575
994
  properties: {
576
995
  sessionId: { type: 'string' },
577
- url: { type: 'string' },
578
- tabId: { type: 'number' },
579
- levels: { type: 'array', items: { type: 'string' } },
580
- contains: { type: 'string' },
996
+ urlContains: { type: 'string' },
997
+ urlRegex: { type: 'string' },
998
+ exactUrl: { type: 'string' },
999
+ timeoutMs: { type: 'number' },
1000
+ pollIntervalMs: { type: 'number' },
1001
+ },
1002
+ },
1003
+ wait_for_navigation: {
1004
+ type: 'object',
1005
+ required: ['sessionId'],
1006
+ properties: {
1007
+ sessionId: { type: 'string' },
1008
+ urlContains: { type: 'string' },
1009
+ urlRegex: { type: 'string' },
1010
+ exactUrl: { type: 'string' },
1011
+ fromUrlContains: { type: 'string' },
1012
+ fromUrlRegex: { type: 'string' },
1013
+ trigger: { type: 'string' },
581
1014
  sinceTs: { type: 'number' },
582
- includeRuntimeErrors: { type: 'boolean' },
583
- dedupeWindowMs: { type: 'number' },
584
- limit: { type: 'number' },
585
- responseProfile: { type: 'string' },
586
- includeArgs: { type: 'boolean' },
587
- maxResponseBytes: { type: 'number' },
1015
+ tabId: { type: 'number' },
1016
+ timeoutMs: { type: 'number' },
1017
+ pollIntervalMs: { type: 'number' },
588
1018
  },
589
1019
  },
590
- list_override_profiles: {
1020
+ wait_for_navigation_lifecycle: {
591
1021
  type: 'object',
592
- properties: {},
1022
+ required: ['sessionId'],
1023
+ properties: {
1024
+ sessionId: { type: 'string' },
1025
+ state: { type: 'string', enum: ['commit', 'same_document', 'domcontentloaded', 'load', 'network_idle'] },
1026
+ urlContains: { type: 'string' },
1027
+ urlRegex: { type: 'string' },
1028
+ exactUrl: { type: 'string' },
1029
+ tabId: { type: 'number' },
1030
+ timeoutMs: { type: 'number' },
1031
+ pollIntervalMs: { type: 'number' },
1032
+ },
593
1033
  },
594
- create_override_profile: {
1034
+ wait_for_load_state: {
595
1035
  type: 'object',
596
- required: ['targetBaseUrl'],
1036
+ required: ['sessionId'],
597
1037
  properties: {
598
- adapter: { type: 'string' },
599
- mode: { type: 'string' },
600
- targetBaseUrl: { type: 'string' },
601
- projectRoot: { type: 'string' },
602
- assetRoot: { type: 'string' },
603
- nextDir: { type: 'string' },
604
- configPath: { type: 'string' },
605
- profileId: { type: 'string' },
606
- profileName: { type: 'string' },
607
- enabled: { type: 'boolean' },
608
- profileEnabled: { type: 'boolean' },
609
- autoReload: { type: 'boolean' },
610
- includeManifestFiles: { type: 'boolean' },
611
- includeStaticFiles: { type: 'boolean' },
612
- extensions: { type: 'array', items: { type: 'string' } },
613
- maxRules: { type: 'number' },
614
- writeConfig: { type: 'boolean' },
615
- overwrite: { type: 'boolean' },
1038
+ sessionId: { type: 'string' },
1039
+ state: { type: 'string', enum: ['domcontentloaded', 'load'] },
1040
+ urlContains: { type: 'string' },
1041
+ urlRegex: { type: 'string' },
1042
+ exactUrl: { type: 'string' },
1043
+ timeoutMs: { type: 'number' },
1044
+ pollIntervalMs: { type: 'number' },
616
1045
  },
617
1046
  },
618
- validate_override_profile: {
1047
+ wait_for_selector_state: {
619
1048
  type: 'object',
1049
+ required: ['sessionId', 'selector'],
620
1050
  properties: {
621
- profileId: { type: 'string' },
1051
+ sessionId: { type: 'string' },
1052
+ selector: { type: 'string' },
1053
+ state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
1054
+ frameId: { type: 'number' },
1055
+ timeoutMs: { type: 'number' },
1056
+ pollIntervalMs: { type: 'number' },
622
1057
  },
623
1058
  },
624
- preflight_overrides: {
1059
+ wait_for_console: {
625
1060
  type: 'object',
626
1061
  required: ['sessionId'],
627
1062
  properties: {
628
1063
  sessionId: { type: 'string' },
629
- profileId: { type: 'string' },
1064
+ levels: { type: 'array', items: { type: 'string' } },
1065
+ contains: { type: 'string' },
1066
+ sinceTs: { type: 'number' },
1067
+ includeRuntimeErrors: { type: 'boolean' },
1068
+ timeoutMs: { type: 'number' },
1069
+ pollIntervalMs: { type: 'number' },
630
1070
  },
631
1071
  },
632
- observe_override_assets: {
1072
+ wait_for_dialog: {
633
1073
  type: 'object',
634
1074
  required: ['sessionId'],
635
1075
  properties: {
636
1076
  sessionId: { type: 'string' },
1077
+ type: { type: 'string', enum: ['alert', 'confirm', 'prompt', 'beforeunload'] },
1078
+ messageContains: { type: 'string' },
1079
+ urlContains: { type: 'string' },
1080
+ action: { type: 'string', enum: ['none', 'accept', 'dismiss'] },
1081
+ promptText: { type: 'string' },
637
1082
  tabId: { type: 'number' },
638
- includePerformance: { type: 'boolean' },
1083
+ timeoutMs: { type: 'number' },
1084
+ pollIntervalMs: { type: 'number' },
639
1085
  },
640
1086
  },
641
- capture_override_response_body: {
1087
+ wait_for_stable_layout: {
642
1088
  type: 'object',
643
1089
  required: ['sessionId'],
644
1090
  properties: {
645
1091
  sessionId: { type: 'string' },
1092
+ selector: { type: 'string' },
1093
+ stableMs: { type: 'number' },
646
1094
  tabId: { type: 'number' },
647
- targetUrl: { type: 'string' },
648
- targetAssetUrl: { type: 'string' },
649
- captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
650
- triggerReload: { type: 'boolean' },
651
- matchMode: { type: 'string', enum: ['exact', 'prefix'] },
652
- ruleType: { type: 'string' },
653
- requestMethod: { type: 'string' },
654
- requestHeaders: { type: 'object' },
655
1095
  timeoutMs: { type: 'number' },
656
- maxBodyBytes: { type: 'number' },
657
- includeBody: { type: 'boolean' },
1096
+ pollIntervalMs: { type: 'number' },
658
1097
  },
659
1098
  },
660
- list_observed_override_assets: {
1099
+ wait_for_download: {
661
1100
  type: 'object',
662
1101
  required: ['sessionId'],
663
1102
  properties: {
664
1103
  sessionId: { type: 'string' },
665
- limit: { type: 'number' },
666
- sinceTimestamp: { type: 'number' },
1104
+ urlContains: { type: 'string' },
1105
+ urlRegex: { type: 'string' },
1106
+ exactUrl: { type: 'string' },
1107
+ filenameContains: { type: 'string' },
1108
+ filenameRegex: { type: 'string' },
1109
+ state: { type: 'string', enum: ['started', 'completed'] },
1110
+ tabId: { type: 'number' },
1111
+ timeoutMs: { type: 'number' },
1112
+ pollIntervalMs: { type: 'number' },
667
1113
  },
668
1114
  },
669
- map_next_override_assets: {
1115
+ wait_for_popup: {
670
1116
  type: 'object',
671
- required: ['projectRoot'],
1117
+ required: ['sessionId'],
672
1118
  properties: {
673
1119
  sessionId: { type: 'string' },
674
- tabId: { type: 'number' },
675
- projectRoot: { type: 'string' },
676
- nextDir: { type: 'string' },
677
- route: { type: 'string' },
678
- sourcePaths: { type: 'array', items: { type: 'string' } },
679
- observedAssets: { type: 'array', items: { type: 'object' } },
680
- maxResults: { type: 'number' },
681
- fetchProductionAssets: { type: 'boolean' },
682
- productionFetchTimeoutMs: { type: 'number' },
683
- maxProductionAssetBytes: { type: 'number' },
684
- maxDriftCandidates: { type: 'number' },
685
- productionFetchConcurrency: { type: 'number' },
1120
+ urlContains: { type: 'string' },
1121
+ urlRegex: { type: 'string' },
1122
+ exactUrl: { type: 'string' },
1123
+ openerTabId: { type: 'number' },
1124
+ timeoutMs: { type: 'number' },
1125
+ pollIntervalMs: { type: 'number' },
686
1126
  },
687
1127
  },
688
- plan_override_response_patch: {
1128
+ wait_for_request: {
1129
+ type: 'object',
1130
+ required: ['sessionId'],
1131
+ properties: {
1132
+ sessionId: { type: 'string' },
1133
+ urlContains: { type: 'string' },
1134
+ urlRegex: { type: 'string' },
1135
+ exactUrl: { type: 'string' },
1136
+ method: { type: 'string' },
1137
+ traceId: { type: 'string' },
1138
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
1139
+ requestContentType: { type: 'string' },
1140
+ sinceTs: { type: 'number' },
1141
+ tabId: { type: 'number' },
1142
+ includeBodies: { type: 'boolean' },
1143
+ timeoutMs: { type: 'number' },
1144
+ pollIntervalMs: { type: 'number' },
1145
+ },
1146
+ },
1147
+ wait_for_response: {
1148
+ type: 'object',
1149
+ required: ['sessionId'],
1150
+ properties: {
1151
+ sessionId: { type: 'string' },
1152
+ urlContains: { type: 'string' },
1153
+ urlRegex: { type: 'string' },
1154
+ exactUrl: { type: 'string' },
1155
+ method: { type: 'string' },
1156
+ traceId: { type: 'string' },
1157
+ initiator: { type: 'string', enum: ['fetch', 'xhr', 'img', 'script', 'other'] },
1158
+ requestContentType: { type: 'string' },
1159
+ responseContentType: { type: 'string' },
1160
+ statusIn: { type: 'array', items: { type: 'number' } },
1161
+ statusGte: { type: 'number' },
1162
+ statusLt: { type: 'number' },
1163
+ errorType: { type: 'string' },
1164
+ sinceTs: { type: 'number' },
1165
+ tabId: { type: 'number' },
1166
+ includeBodies: { type: 'boolean' },
1167
+ timeoutMs: { type: 'number' },
1168
+ pollIntervalMs: { type: 'number' },
1169
+ },
1170
+ },
1171
+ wait_for_network_quiet: {
1172
+ type: 'object',
1173
+ required: ['sessionId'],
1174
+ properties: {
1175
+ sessionId: { type: 'string' },
1176
+ quietMs: { type: 'number' },
1177
+ urlContains: { type: 'string' },
1178
+ method: { type: 'string' },
1179
+ tabId: { type: 'number' },
1180
+ timeoutMs: { type: 'number' },
1181
+ pollIntervalMs: { type: 'number' },
1182
+ },
1183
+ },
1184
+ capture_ui_snapshot: {
1185
+ type: 'object',
1186
+ required: ['sessionId'],
1187
+ properties: {
1188
+ sessionId: { type: 'string' },
1189
+ selector: { type: 'string' },
1190
+ trigger: { type: 'string' },
1191
+ mode: { type: 'string' },
1192
+ styleMode: { type: 'string' },
1193
+ maxDepth: { type: 'number' },
1194
+ maxBytes: { type: 'number' },
1195
+ maxAncestors: { type: 'number' },
1196
+ includeDom: { type: 'boolean' },
1197
+ includeStyles: { type: 'boolean' },
1198
+ includePngDataUrl: { type: 'boolean' },
1199
+ },
1200
+ },
1201
+ get_live_console_logs: {
1202
+ type: 'object',
1203
+ required: ['sessionId'],
1204
+ properties: {
1205
+ sessionId: { type: 'string' },
1206
+ url: { type: 'string' },
1207
+ tabId: { type: 'number' },
1208
+ levels: { type: 'array', items: { type: 'string' } },
1209
+ contains: { type: 'string' },
1210
+ sinceTs: { type: 'number' },
1211
+ includeRuntimeErrors: { type: 'boolean' },
1212
+ dedupeWindowMs: { type: 'number' },
1213
+ limit: { type: 'number' },
1214
+ responseProfile: { type: 'string' },
1215
+ includeArgs: { type: 'boolean' },
1216
+ maxResponseBytes: { type: 'number' },
1217
+ },
1218
+ },
1219
+ list_override_profiles: {
1220
+ type: 'object',
1221
+ properties: {
1222
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1223
+ },
1224
+ },
1225
+ create_override_profile: {
1226
+ type: 'object',
1227
+ required: ['targetBaseUrl'],
1228
+ properties: {
1229
+ adapter: { type: 'string' },
1230
+ mode: { type: 'string' },
1231
+ targetBaseUrl: { type: 'string' },
1232
+ projectRoot: { type: 'string' },
1233
+ assetRoot: { type: 'string' },
1234
+ nextDir: { type: 'string' },
1235
+ configPath: { type: 'string' },
1236
+ profileId: { type: 'string' },
1237
+ profileName: { type: 'string' },
1238
+ enabled: { type: 'boolean' },
1239
+ profileEnabled: { type: 'boolean' },
1240
+ autoReload: { type: 'boolean' },
1241
+ includeManifestFiles: { type: 'boolean' },
1242
+ includeStaticFiles: { type: 'boolean' },
1243
+ extensions: { type: 'array', items: { type: 'string' } },
1244
+ maxRules: { type: 'number' },
1245
+ writeConfig: { type: 'boolean' },
1246
+ overwrite: { type: 'boolean' },
1247
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1248
+ includeConfigJson: { type: 'boolean' },
1249
+ },
1250
+ },
1251
+ validate_override_profile: {
1252
+ type: 'object',
1253
+ properties: {
1254
+ profileId: { type: 'string' },
1255
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1256
+ },
1257
+ },
1258
+ preflight_overrides: {
1259
+ type: 'object',
1260
+ required: ['sessionId'],
1261
+ properties: {
1262
+ sessionId: { type: 'string' },
1263
+ profileId: { type: 'string' },
1264
+ },
1265
+ },
1266
+ observe_override_assets: {
1267
+ type: 'object',
1268
+ required: ['sessionId'],
1269
+ properties: {
1270
+ sessionId: { type: 'string' },
1271
+ tabId: { type: 'number' },
1272
+ includePerformance: { type: 'boolean' },
1273
+ },
1274
+ },
1275
+ capture_override_response_body: {
1276
+ type: 'object',
1277
+ required: ['sessionId'],
1278
+ properties: {
1279
+ sessionId: { type: 'string' },
1280
+ tabId: { type: 'number' },
1281
+ targetUrl: { type: 'string' },
1282
+ targetAssetUrl: { type: 'string' },
1283
+ captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
1284
+ triggerReload: { type: 'boolean' },
1285
+ matchMode: { type: 'string', enum: ['exact', 'prefix'] },
1286
+ ruleType: { type: 'string' },
1287
+ requestMethod: { type: 'string' },
1288
+ requestHeaders: { type: 'object' },
1289
+ timeoutMs: { type: 'number' },
1290
+ maxBodyBytes: { type: 'number' },
1291
+ includeBody: { type: 'boolean' },
1292
+ },
1293
+ },
1294
+ list_observed_override_assets: {
1295
+ type: 'object',
1296
+ required: ['sessionId'],
1297
+ properties: {
1298
+ sessionId: { type: 'string' },
1299
+ limit: { type: 'number' },
1300
+ sinceTimestamp: { type: 'number' },
1301
+ responseProfile: { type: 'string', enum: ['compact', 'full'] },
1302
+ },
1303
+ },
1304
+ map_next_override_assets: {
1305
+ type: 'object',
1306
+ required: ['projectRoot'],
1307
+ properties: {
1308
+ sessionId: { type: 'string' },
1309
+ tabId: { type: 'number' },
1310
+ projectRoot: { type: 'string' },
1311
+ nextDir: { type: 'string' },
1312
+ route: { type: 'string' },
1313
+ sourcePaths: { type: 'array', items: { type: 'string' } },
1314
+ observedAssets: { type: 'array', items: { type: 'object' } },
1315
+ maxResults: { type: 'number' },
1316
+ fetchProductionAssets: { type: 'boolean' },
1317
+ productionFetchTimeoutMs: { type: 'number' },
1318
+ maxProductionAssetBytes: { type: 'number' },
1319
+ maxDriftCandidates: { type: 'number' },
1320
+ productionFetchConcurrency: { type: 'number' },
1321
+ },
1322
+ },
1323
+ plan_override_response_patch: {
689
1324
  type: 'object',
690
1325
  properties: {
691
1326
  sessionId: { type: 'string' },
@@ -862,7 +1497,7 @@ const TOOL_SCHEMAS = {
862
1497
  properties: {
863
1498
  sessionId: { type: 'string' },
864
1499
  status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
865
- action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
1500
+ action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
866
1501
  traceId: { type: 'string' },
867
1502
  limit: { type: 'number' },
868
1503
  offset: { type: 'number' },
@@ -885,16 +1520,53 @@ const TOOL_SCHEMAS = {
885
1520
  required: ['sessionId', 'action'],
886
1521
  properties: {
887
1522
  sessionId: { type: 'string' },
888
- action: { type: 'string', enum: ['click', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
1523
+ action: { type: 'string', enum: ['click', 'hover', 'input', 'focus', 'blur', 'scroll', 'press_key', 'submit', 'reload'] },
889
1524
  traceId: { type: 'string' },
890
1525
  target: {
891
1526
  type: 'object',
892
1527
  properties: {
893
1528
  selector: { type: 'string' },
894
1529
  elementRef: { type: 'string' },
1530
+ coordinates: {
1531
+ type: 'object',
1532
+ properties: {
1533
+ x: { type: 'number' },
1534
+ y: { type: 'number' },
1535
+ frameId: { type: 'number' },
1536
+ },
1537
+ },
895
1538
  tabId: { type: 'number' },
896
1539
  frameId: { type: 'number' },
897
1540
  url: { type: 'string' },
1541
+ locator: ACTION_LOCATOR_TOOL_SCHEMA,
1542
+ frameUrlContains: { type: 'string' },
1543
+ frameTitleContains: { type: 'string' },
1544
+ testId: { type: 'string' },
1545
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
1546
+ textContains: { type: 'string' },
1547
+ labelContains: { type: 'string' },
1548
+ titleContains: { type: 'string' },
1549
+ role: { type: 'string' },
1550
+ name: { type: 'string' },
1551
+ placeholder: { type: 'string' },
1552
+ altText: { type: 'string' },
1553
+ exact: { type: 'boolean' },
1554
+ nth: { type: 'number' },
1555
+ first: { type: 'boolean' },
1556
+ last: { type: 'boolean' },
1557
+ strict: { type: 'boolean' },
1558
+ tagName: { type: 'string' },
1559
+ type: { type: 'string' },
1560
+ visible: { type: 'boolean' },
1561
+ enabled: { type: 'boolean' },
1562
+ disabled: { type: 'boolean' },
1563
+ editable: { type: 'boolean' },
1564
+ checked: { type: 'boolean' },
1565
+ selected: { type: 'boolean' },
1566
+ pressed: { type: 'boolean' },
1567
+ expanded: { type: 'boolean' },
1568
+ readOnly: { type: 'boolean' },
1569
+ requiredField: { type: 'boolean' },
898
1570
  },
899
1571
  },
900
1572
  input: { type: 'object' },
@@ -917,14 +1589,22 @@ const TOOL_SCHEMAS = {
917
1589
  type: 'object',
918
1590
  required: ['scope'],
919
1591
  properties: {
920
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1592
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
921
1593
  selector: { type: 'string' },
922
1594
  testId: { type: 'string' },
923
1595
  textContains: { type: 'string' },
924
1596
  labelContains: { type: 'string' },
925
1597
  titleContains: { type: 'string' },
1598
+ role: { type: 'string' },
1599
+ name: { type: 'string' },
1600
+ placeholder: { type: 'string' },
1601
+ altText: { type: 'string' },
1602
+ exact: { type: 'boolean' },
1603
+ frameUrlContains: { type: 'string' },
1604
+ frameTitleContains: { type: 'string' },
926
1605
  urlContains: { type: 'string' },
927
1606
  language: { type: 'string' },
1607
+ visible: { type: 'boolean' },
928
1608
  disabled: { type: 'boolean' },
929
1609
  selected: { type: 'boolean' },
930
1610
  pressed: { type: 'boolean' },
@@ -961,7 +1641,7 @@ const TOOL_SCHEMAS = {
961
1641
  properties: {
962
1642
  id: { type: 'string' },
963
1643
  note: { type: 'string' },
964
- kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
1644
+ kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
965
1645
  action: { type: 'string' },
966
1646
  traceId: { type: 'string' },
967
1647
  target: {
@@ -969,16 +1649,37 @@ const TOOL_SCHEMAS = {
969
1649
  properties: {
970
1650
  selector: { type: 'string' },
971
1651
  elementRef: { type: 'string' },
1652
+ coordinates: {
1653
+ type: 'object',
1654
+ properties: {
1655
+ x: { type: 'number' },
1656
+ y: { type: 'number' },
1657
+ frameId: { type: 'number' },
1658
+ },
1659
+ },
972
1660
  tabId: { type: 'number' },
973
1661
  frameId: { type: 'number' },
974
1662
  url: { type: 'string' },
1663
+ locator: ACTION_LOCATOR_TOOL_SCHEMA,
1664
+ frameUrlContains: { type: 'string' },
1665
+ frameTitleContains: { type: 'string' },
975
1666
  testId: { type: 'string' },
976
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
1667
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
977
1668
  textContains: { type: 'string' },
978
1669
  labelContains: { type: 'string' },
979
1670
  titleContains: { type: 'string' },
1671
+ role: { type: 'string' },
1672
+ name: { type: 'string' },
1673
+ placeholder: { type: 'string' },
1674
+ altText: { type: 'string' },
1675
+ exact: { type: 'boolean' },
1676
+ nth: { type: 'number' },
1677
+ first: { type: 'boolean' },
1678
+ last: { type: 'boolean' },
1679
+ strict: { type: 'boolean' },
980
1680
  tagName: { type: 'string' },
981
1681
  type: { type: 'string' },
1682
+ visible: { type: 'boolean' },
982
1683
  disabled: { type: 'boolean' },
983
1684
  selected: { type: 'boolean' },
984
1685
  pressed: { type: 'boolean' },
@@ -1012,14 +1713,22 @@ const TOOL_SCHEMAS = {
1012
1713
  matcher: {
1013
1714
  type: 'object',
1014
1715
  properties: {
1015
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1716
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
1016
1717
  selector: { type: 'string' },
1017
1718
  testId: { type: 'string' },
1018
1719
  textContains: { type: 'string' },
1019
1720
  labelContains: { type: 'string' },
1020
1721
  titleContains: { type: 'string' },
1722
+ role: { type: 'string' },
1723
+ name: { type: 'string' },
1724
+ placeholder: { type: 'string' },
1725
+ altText: { type: 'string' },
1726
+ exact: { type: 'boolean' },
1727
+ frameUrlContains: { type: 'string' },
1728
+ frameTitleContains: { type: 'string' },
1021
1729
  urlContains: { type: 'string' },
1022
1730
  language: { type: 'string' },
1731
+ visible: { type: 'boolean' },
1023
1732
  disabled: { type: 'boolean' },
1024
1733
  selected: { type: 'boolean' },
1025
1734
  pressed: { type: 'boolean' },
@@ -1036,6 +1745,7 @@ const TOOL_SCHEMAS = {
1036
1745
  pollIntervalMs: { type: 'number' },
1037
1746
  },
1038
1747
  },
1748
+ wait: AUTOMATION_WAIT_TOOL_SCHEMA,
1039
1749
  },
1040
1750
  },
1041
1751
  },
@@ -1062,11 +1772,25 @@ const TOOL_DESCRIPTIONS = {
1062
1772
  get_computed_styles: 'Read computed CSS styles for an element',
1063
1773
  get_layout_metrics: 'Read viewport and element layout metrics',
1064
1774
  get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
1065
- get_interactive_elements: 'Read compact live element references for buttons, inputs, modals, and focused elements',
1775
+ get_interactive_elements: 'Read compact live element references for buttons, links, inputs, modals, and focused elements',
1066
1776
  get_live_session_health: 'Read live transport health and session binding details for one session',
1067
1777
  set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
1068
1778
  assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
1069
1779
  wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
1780
+ preflight_automation_flow: 'Check live-session readiness and production risks before running an automation flow',
1781
+ wait_for_url: 'Poll the live page URL until it matches an exact, contains, or regex condition',
1782
+ wait_for_navigation: 'Poll persisted navigation events until a matching URL or trigger is observed',
1783
+ wait_for_navigation_lifecycle: 'Wait for a live navigation lifecycle milestone such as commit, load, or network idle',
1784
+ wait_for_load_state: 'Poll the live page document readiness until domcontentloaded or load is reached',
1785
+ wait_for_selector_state: 'Poll a selector until it is attached, detached, visible, or hidden',
1786
+ wait_for_console: 'Poll live console logs until a matching message appears',
1787
+ wait_for_dialog: 'Wait for a native JavaScript dialog and optionally accept or dismiss it',
1788
+ wait_for_stable_layout: 'Wait until the page or selector layout stays unchanged for a stable window',
1789
+ wait_for_download: 'Wait for a download started by the bound tab and optionally until completion',
1790
+ wait_for_popup: 'Wait for a popup tab or window opened from the bound session tab',
1791
+ wait_for_network_quiet: 'Wait until persisted network activity is quiet for a bounded window',
1792
+ wait_for_request: 'Poll persisted network activity until a matching request is observed',
1793
+ wait_for_response: 'Poll persisted network activity until a matching response is observed',
1070
1794
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
1071
1795
  get_live_console_logs: 'Read in-memory live console logs for a connected session',
1072
1796
  list_override_profiles: 'List configured browser override profiles',
@@ -1113,6 +1837,7 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
1113
1837
  const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
1114
1838
  const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
1115
1839
  const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
1840
+ const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
1116
1841
  const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
1117
1842
  const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
1118
1843
  const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
@@ -1395,7 +2120,7 @@ function buildOverrideProfileRecords() {
1395
2120
  active: profile.profileId === summary.activeProfileId,
1396
2121
  configEnabled: summary.configEnabled,
1397
2122
  enabled: profile.enabled,
1398
- effectiveEnabled: summary.configEnabled && profile.enabled && profile.enabledRuleCount > 0,
2123
+ effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
1399
2124
  autoReload: profile.autoReload,
1400
2125
  configPath: summary.configPath,
1401
2126
  fileExists: profile.fileExists,
@@ -1414,6 +2139,72 @@ function resolveOverrideProfileRecord(value) {
1414
2139
  }
1415
2140
  return profile;
1416
2141
  }
2142
+ function resolveOverrideResponseProfile(value) {
2143
+ return value === 'full' ? 'full' : 'compact';
2144
+ }
2145
+ function compactOverrideRule(rule) {
2146
+ if (!isRecord(rule)) {
2147
+ return {};
2148
+ }
2149
+ return {
2150
+ ruleId: rule.ruleId,
2151
+ enabled: rule.enabled,
2152
+ ruleType: rule.ruleType,
2153
+ requestMethod: rule.requestMethod,
2154
+ matchMode: rule.matchMode,
2155
+ targetAssetUrl: rule.targetAssetUrl,
2156
+ localFilePath: rule.localFilePath,
2157
+ contentType: rule.contentType,
2158
+ fileExists: rule.fileExists,
2159
+ integrity: rule.integrity,
2160
+ };
2161
+ }
2162
+ function compactOverrideProfile(profile, ruleLimit = 10) {
2163
+ const rules = Array.isArray(profile.rules) ? profile.rules : [];
2164
+ return {
2165
+ profileId: profile.profileId,
2166
+ name: profile.name,
2167
+ active: profile.active,
2168
+ configEnabled: profile.configEnabled,
2169
+ enabled: profile.enabled,
2170
+ effectiveEnabled: profile.effectiveEnabled,
2171
+ autoReload: profile.autoReload,
2172
+ configPath: profile.configPath,
2173
+ fileExists: profile.fileExists,
2174
+ ruleCount: profile.ruleCount,
2175
+ enabledRuleCount: profile.enabledRuleCount,
2176
+ rules: rules.slice(0, ruleLimit).map(compactOverrideRule),
2177
+ rulesOmitted: Math.max(0, rules.length - ruleLimit),
2178
+ };
2179
+ }
2180
+ function serializeOverrideProfile(profile, responseProfile) {
2181
+ return responseProfile === 'full' ? profile : compactOverrideProfile(profile);
2182
+ }
2183
+ function compactObservedOverrideAsset(asset) {
2184
+ if (!isRecord(asset)) {
2185
+ return {};
2186
+ }
2187
+ return {
2188
+ observedAssetId: asset.observedAssetId,
2189
+ lastSeenAt: asset.lastSeenAt,
2190
+ tabId: asset.tabId,
2191
+ url: asset.url,
2192
+ ruleType: asset.ruleType,
2193
+ requestMethod: asset.requestMethod,
2194
+ resourceType: asset.resourceType,
2195
+ contentType: asset.contentType,
2196
+ statusCode: asset.statusCode,
2197
+ pathname: asset.pathname,
2198
+ assetPath: asset.assetPath,
2199
+ kind: asset.kind,
2200
+ integrity: asset.integrity,
2201
+ fromDom: asset.fromDom,
2202
+ fromPerformance: asset.fromPerformance,
2203
+ fromNavigation: asset.fromNavigation,
2204
+ fromFetch: asset.fromFetch,
2205
+ serviceWorkerControlled: asset.serviceWorkerControlled,
2206
+ };
2207
+ }
1417
2208
  const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
1418
2209
  function sha256Text(value) {
1419
2210
  return createHash('sha256').update(value, 'utf8').digest('hex');
@@ -1582,13 +2373,6 @@ function buildOverrideProfileIssues(profile) {
1582
2373
  const rules = Array.isArray(profile.rules)
1583
2374
  ? profile.rules.filter((rule) => isRecord(rule))
1584
2375
  : [];
1585
- if (profile.configEnabled !== true) {
1586
- issues.push({
1587
- code: 'CONFIG_DISABLED',
1588
- severity: 'warning',
1589
- message: 'The override config is disabled and cannot replace requests until enabled.',
1590
- });
1591
- }
1592
2376
  if (profile.enabled !== true) {
1593
2377
  issues.push({
1594
2378
  code: 'PROFILE_DISABLED',
@@ -1677,12 +2461,6 @@ function buildOverrideProfileNextActions(profile, issues) {
1677
2461
  message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
1678
2462
  }];
1679
2463
  }
1680
- if (profile.configEnabled !== true) {
1681
- return [{
1682
- code: 'ENABLE_CONFIG',
1683
- message: 'Set the root override config enabled=true after reviewing the profile.',
1684
- }];
1685
- }
1686
2464
  if (profile.enabled !== true) {
1687
2465
  return [{
1688
2466
  code: 'ENABLE_PROFILE',
@@ -2558,6 +3336,7 @@ function mapAutomationRunRecord(row) {
2558
3336
  : undefined,
2559
3337
  stopReason: row.stop_reason ?? undefined,
2560
3338
  target: parseJsonOrUndefined(row.target_summary_json),
3339
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2561
3340
  failure: parseJsonOrUndefined(row.failure_json),
2562
3341
  redaction: parseJsonOrUndefined(row.redaction_json),
2563
3342
  stepCount: row.step_count,
@@ -2582,6 +3361,7 @@ function mapAutomationStepRecord(row) {
2582
3361
  durationMs: row.duration_ms ?? undefined,
2583
3362
  tabId: row.tab_id ?? undefined,
2584
3363
  target: parseJsonOrUndefined(row.target_summary_json),
3364
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2585
3365
  redaction: parseJsonOrUndefined(row.redaction_json),
2586
3366
  failure: parseJsonOrUndefined(row.failure_json),
2587
3367
  inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
@@ -2592,6 +3372,109 @@ function mapAutomationStepRecord(row) {
2592
3372
  source: 'automation_steps',
2593
3373
  };
2594
3374
  }
3375
+ function asRecordOrUndefined(value) {
3376
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3377
+ ? value
3378
+ : undefined;
3379
+ }
3380
+ function buildFailureEvidenceSummary(failureEvidence) {
3381
+ if (!failureEvidence) {
3382
+ return undefined;
3383
+ }
3384
+ const snapshot = asRecordOrUndefined(failureEvidence.snapshot);
3385
+ const snapshotRoot = asRecordOrUndefined(snapshot?.snapshot);
3386
+ return {
3387
+ captured: failureEvidence.captured === true,
3388
+ error: typeof failureEvidence.error === 'string' ? failureEvidence.error : undefined,
3389
+ limitsApplied: asRecordOrUndefined(failureEvidence.limitsApplied),
3390
+ snapshot: snapshot
3391
+ ? {
3392
+ timestamp: typeof snapshot.timestamp === 'number' ? snapshot.timestamp : undefined,
3393
+ trigger: typeof snapshot.trigger === 'string' ? snapshot.trigger : undefined,
3394
+ selector: typeof snapshot.selector === 'string' ? snapshot.selector : undefined,
3395
+ url: typeof snapshot.url === 'string' ? snapshot.url : undefined,
3396
+ mode: snapshot.mode,
3397
+ hasDom: Boolean(snapshotRoot && 'dom' in snapshotRoot),
3398
+ hasStyles: Boolean(snapshotRoot && 'styles' in snapshotRoot),
3399
+ hasPng: Boolean(snapshot.png),
3400
+ }
3401
+ : undefined,
3402
+ };
3403
+ }
3404
+ function findRelatedFailureSnapshot(db, sessionId, failureEvidence) {
3405
+ const snapshotSummary = asRecordOrUndefined(buildFailureEvidenceSummary(failureEvidence)?.snapshot);
3406
+ if (!snapshotSummary) {
3407
+ return undefined;
3408
+ }
3409
+ const timestamp = typeof snapshotSummary.timestamp === 'number' ? snapshotSummary.timestamp : undefined;
3410
+ const selector = typeof snapshotSummary.selector === 'string' ? snapshotSummary.selector : undefined;
3411
+ const url = typeof snapshotSummary.url === 'string' ? snapshotSummary.url : undefined;
3412
+ const where = ['session_id = ?'];
3413
+ const params = [sessionId];
3414
+ if (selector) {
3415
+ where.push('selector = ?');
3416
+ params.push(selector);
3417
+ }
3418
+ if (url) {
3419
+ where.push('url = ?');
3420
+ params.push(url);
3421
+ }
3422
+ if (timestamp !== undefined) {
3423
+ where.push('ts BETWEEN ? AND ?');
3424
+ params.push(timestamp - 10_000, timestamp + 10_000);
3425
+ }
3426
+ const row = db.prepare(`SELECT snapshot_id, trigger_event_id, ts, selector, url
3427
+ FROM snapshots
3428
+ WHERE ${where.join(' AND ')}
3429
+ ORDER BY ${timestamp !== undefined ? 'ABS(ts - ?) ASC,' : ''} ts DESC
3430
+ LIMIT 1`).get(...params, ...(timestamp !== undefined ? [timestamp] : []));
3431
+ if (!row) {
3432
+ return undefined;
3433
+ }
3434
+ return {
3435
+ snapshotId: row.snapshot_id,
3436
+ triggerEventId: row.trigger_event_id ?? undefined,
3437
+ timestamp: row.ts,
3438
+ selector: row.selector ?? undefined,
3439
+ url: row.url ?? undefined,
3440
+ };
3441
+ }
3442
+ function mergeAutomationDiagnosticsEvidence(db, options) {
3443
+ if (!options.traceId) {
3444
+ return;
3445
+ }
3446
+ const failureEvidence = buildFailureEvidenceSummary(options.failureEvidence);
3447
+ const linkedSnapshot = findRelatedFailureSnapshot(db, options.sessionId, options.failureEvidence);
3448
+ if (!failureEvidence && !linkedSnapshot && !options.cdpFailure) {
3449
+ return;
3450
+ }
3451
+ const updateDiagnosticsJson = (tableName, keyColumn, keyValue, existingJson) => {
3452
+ const existing = asRecordOrUndefined(parseJsonOrUndefined(existingJson)) ?? {};
3453
+ const merged = {
3454
+ ...existing,
3455
+ ...(options.cdpFailure ? { cdpFailure: options.cdpFailure } : {}),
3456
+ ...(failureEvidence ? { failureEvidence } : {}),
3457
+ ...(linkedSnapshot ? { linkedSnapshot } : {}),
3458
+ };
3459
+ db.prepare(`UPDATE ${tableName} SET diagnostics_json = ?, updated_at = ? WHERE ${keyColumn} = ?`).run(JSON.stringify(merged), Date.now(), keyValue);
3460
+ };
3461
+ const runRow = db.prepare(`SELECT run_id, diagnostics_json
3462
+ FROM automation_runs
3463
+ WHERE session_id = ? AND trace_id = ?
3464
+ ORDER BY started_at DESC, updated_at DESC
3465
+ LIMIT 1`).get(options.sessionId, options.traceId);
3466
+ if (runRow) {
3467
+ updateDiagnosticsJson('automation_runs', 'run_id', runRow.run_id, runRow.diagnostics_json);
3468
+ }
3469
+ const stepRow = db.prepare(`SELECT step_id, diagnostics_json
3470
+ FROM automation_steps
3471
+ WHERE session_id = ? AND trace_id = ?
3472
+ ORDER BY step_order DESC, updated_at DESC
3473
+ LIMIT 1`).get(options.sessionId, options.traceId);
3474
+ if (stepRow) {
3475
+ updateDiagnosticsJson('automation_steps', 'step_id', stepRow.step_id, stepRow.diagnostics_json);
3476
+ }
3477
+ }
2595
3478
  function formatUrlPath(url) {
2596
3479
  try {
2597
3480
  const parsed = new URL(url);
@@ -2724,15 +3607,25 @@ function resolveViewportDimension(value, axis) {
2724
3607
  }
2725
3608
  return floored;
2726
3609
  }
2727
- class WorkflowTargetResolutionError extends Error {
2728
- code;
2729
- details;
2730
- constructor(code, message, details) {
2731
- super(message);
2732
- this.name = 'WorkflowTargetResolutionError';
2733
- this.code = code;
2734
- this.details = details;
3610
+ function buildWaitTimeoutDiagnostics(options) {
3611
+ const diagnostics = {
3612
+ waitKind: options.waitKind,
3613
+ timeoutMs: options.timeoutMs,
3614
+ waitedMs: options.waitedMs,
3615
+ attempts: options.attempts,
3616
+ pollIntervalMs: options.pollIntervalMs,
3617
+ matcherSummary: options.matcherSummary,
3618
+ };
3619
+ if (options.lastObserved !== undefined) {
3620
+ diagnostics.lastObserved = options.lastObserved;
2735
3621
  }
3622
+ if (typeof options.candidateCount === 'number') {
3623
+ diagnostics.candidateCount = options.candidateCount;
3624
+ }
3625
+ if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
3626
+ diagnostics.sampledCandidates = options.sampledCandidates;
3627
+ }
3628
+ return diagnostics;
2736
3629
  }
2737
3630
  function resolveOptionalMatcherString(value) {
2738
3631
  if (typeof value !== 'string') {
@@ -2755,10 +3648,10 @@ function resolveOptionalMatcherCount(value, field) {
2755
3648
  return floored;
2756
3649
  }
2757
3650
  function resolvePageStateScope(value) {
2758
- if (value === 'buttons' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
3651
+ if (value === 'buttons' || value === 'links' || value === 'inputs' || value === 'modals' || value === 'focused' || value === 'page') {
2759
3652
  return value;
2760
3653
  }
2761
- throw new Error('scope must be one of buttons, inputs, modals, focused, or page');
3654
+ throw new Error('scope must be one of buttons, links, inputs, modals, focused, or page');
2762
3655
  }
2763
3656
  function resolvePageStateMatcher(input) {
2764
3657
  const matcher = {
@@ -2768,6 +3661,13 @@ function resolvePageStateMatcher(input) {
2768
3661
  textContains: resolveOptionalMatcherString(input.textContains),
2769
3662
  labelContains: resolveOptionalMatcherString(input.labelContains),
2770
3663
  titleContains: resolveOptionalMatcherString(input.titleContains),
3664
+ role: resolveOptionalMatcherString(input.role)?.toLowerCase(),
3665
+ name: resolveOptionalMatcherString(input.name),
3666
+ placeholder: resolveOptionalMatcherString(input.placeholder),
3667
+ altText: resolveOptionalMatcherString(input.altText),
3668
+ exact: resolveOptionalMatcherBoolean(input.exact),
3669
+ frameUrlContains: resolveOptionalMatcherString(input.frameUrlContains),
3670
+ frameTitleContains: resolveOptionalMatcherString(input.frameTitleContains),
2771
3671
  urlContains: resolveOptionalMatcherString(input.urlContains),
2772
3672
  language: resolveOptionalMatcherString(input.language),
2773
3673
  disabled: resolveOptionalMatcherBoolean(input.disabled),
@@ -2778,6 +3678,7 @@ function resolvePageStateMatcher(input) {
2778
3678
  requiredField: resolveOptionalMatcherBoolean(input.requiredField),
2779
3679
  tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
2780
3680
  type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
3681
+ visible: resolveOptionalMatcherBoolean(input.visible),
2781
3682
  countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
2782
3683
  countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
2783
3684
  };
@@ -2792,6 +3693,19 @@ function includesNormalized(value, needle) {
2792
3693
  }
2793
3694
  return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
2794
3695
  }
3696
+ function matchesTextValue(value, expected, exact) {
3697
+ if (!expected) {
3698
+ return true;
3699
+ }
3700
+ if (typeof value !== 'string') {
3701
+ return false;
3702
+ }
3703
+ const normalizedValue = value.trim().toLowerCase();
3704
+ const normalizedExpected = expected.trim().toLowerCase();
3705
+ return exact === true
3706
+ ? normalizedValue === normalizedExpected
3707
+ : normalizedValue.includes(normalizedExpected);
3708
+ }
2795
3709
  function equalsNormalized(value, expected) {
2796
3710
  if (!expected) {
2797
3711
  return true;
@@ -2805,7 +3719,7 @@ function equalsOptionalBoolean(value, expected) {
2805
3719
  return value === expected;
2806
3720
  }
2807
3721
  function pickPageStateScopeItems(payload, scope) {
2808
- if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
3722
+ if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
2809
3723
  const value = payload[scope];
2810
3724
  return asRecordArray(value);
2811
3725
  }
@@ -2818,13 +3732,20 @@ function pickPageStateScopeItems(payload, scope) {
2818
3732
  function matchesPageStateItem(item, matcher) {
2819
3733
  return (includesNormalized(item.selector, matcher.selector)
2820
3734
  && equalsNormalized(item.testId, matcher.testId)
2821
- && includesNormalized(item.text, matcher.textContains)
2822
- && includesNormalized(item.label, matcher.labelContains)
2823
- && includesNormalized(item.title, matcher.titleContains)
3735
+ && matchesTextValue(item.text, matcher.textContains, matcher.exact)
3736
+ && matchesTextValue(item.label, matcher.labelContains, matcher.exact)
3737
+ && matchesTextValue(item.title, matcher.titleContains, matcher.exact)
3738
+ && equalsNormalized(item.role, matcher.role)
3739
+ && matchesTextValue(item.name, matcher.name, matcher.exact)
3740
+ && matchesTextValue(item.placeholder, matcher.placeholder, matcher.exact)
3741
+ && matchesTextValue(item.altText, matcher.altText, matcher.exact)
3742
+ && includesNormalized(item.frameUrl, matcher.frameUrlContains)
3743
+ && includesNormalized(item.frameTitle, matcher.frameTitleContains)
2824
3744
  && includesNormalized(item.url, matcher.urlContains)
2825
3745
  && equalsNormalized(item.language, matcher.language)
2826
3746
  && equalsNormalized(item.tagName, matcher.tagName)
2827
3747
  && equalsNormalized(item.type, matcher.type)
3748
+ && equalsOptionalBoolean(item.visible, matcher.visible)
2828
3749
  && equalsOptionalBoolean(item.disabled, matcher.disabled)
2829
3750
  && equalsOptionalBoolean(item.selected, matcher.selected)
2830
3751
  && equalsOptionalBoolean(item.pressed, matcher.pressed)
@@ -2881,7 +3802,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2881
3802
  const previousSummary = previous?.summary;
2882
3803
  const currentSummary = current.summary;
2883
3804
  const summaryDelta = {};
2884
- for (const key of ['buttons', 'inputs', 'modals']) {
3805
+ for (const key of ['buttons', 'links', 'inputs', 'modals']) {
2885
3806
  const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
2886
3807
  const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
2887
3808
  if (previousValue !== currentValue && currentValue !== undefined) {
@@ -2910,13 +3831,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2910
3831
  }
2911
3832
  function resolveInteractiveKinds(value) {
2912
3833
  if (!Array.isArray(value) || value.length === 0) {
2913
- return ['buttons', 'inputs', 'modals', 'focused'];
3834
+ return ['buttons', 'links', 'inputs', 'modals', 'focused'];
2914
3835
  }
2915
- const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
3836
+ const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
2916
3837
  const kinds = value
2917
3838
  .filter((entry) => typeof entry === 'string' && allowed.has(entry))
2918
3839
  .map((entry) => entry);
2919
- return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'inputs', 'modals', 'focused'];
3840
+ return kinds.length > 0 ? Array.from(new Set(kinds)) : ['buttons', 'links', 'inputs', 'modals', 'focused'];
2920
3841
  }
2921
3842
  function collectInteractiveElementRefs(payload, kinds, maxItems) {
2922
3843
  const refs = [];
@@ -3036,132 +3957,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
3036
3957
  const { lastCapture: _lastCapture, ...waited } = detailed;
3037
3958
  return waited;
3038
3959
  }
3039
- function candidateTextForWorkflowTarget(item) {
3040
- return [item.text, item.label, item.title]
3041
- .filter((value) => typeof value === 'string' && value.trim().length > 0)
3042
- .join(' ')
3043
- .trim();
3960
+ function compileWaitRegex(value, fieldName) {
3961
+ if (!value) {
3962
+ return undefined;
3963
+ }
3964
+ try {
3965
+ return new RegExp(value);
3966
+ }
3967
+ catch {
3968
+ throw new Error(`${fieldName} must be a valid regular expression`);
3969
+ }
3970
+ }
3971
+ function matchesUrlWait(url, wait) {
3972
+ return matchesUrlPredicates(url, {
3973
+ exactUrl: wait.exactUrl,
3974
+ urlContains: wait.urlContains,
3975
+ urlRegex: wait.urlRegex,
3976
+ });
3977
+ }
3978
+ function matchesUrlPredicates(url, predicates) {
3979
+ if (typeof url !== 'string') {
3980
+ return false;
3981
+ }
3982
+ if (predicates.exactUrl && url !== predicates.exactUrl) {
3983
+ return false;
3984
+ }
3985
+ if (predicates.urlContains && !url.includes(predicates.urlContains)) {
3986
+ return false;
3987
+ }
3988
+ const regex = compileWaitRegex(predicates.urlRegex, predicates.regexFieldName ?? 'urlRegex');
3989
+ if (regex && !regex.test(url)) {
3990
+ return false;
3991
+ }
3992
+ return true;
3993
+ }
3994
+ function resolveAutomationWaitSinceTs(value) {
3995
+ return resolveOptionalTimestamp(value) ?? Math.max(0, Date.now() - DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS);
3996
+ }
3997
+ function isElementMissingError(error) {
3998
+ const message = error instanceof Error ? error.message : String(error);
3999
+ return /no element found for selector/i.test(message);
3044
4000
  }
3045
- function describeWorkflowTargetCandidate(item) {
4001
+ async function captureSelectorState(captureClient, sessionId, selector, frameId = 0) {
4002
+ try {
4003
+ const [styleCapture, layoutCapture] = await Promise.all([
4004
+ executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties: ['display', 'visibility', 'opacity'] }, 3_000),
4005
+ executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000),
4006
+ ]);
4007
+ const stylePayload = ensureCaptureSuccess(styleCapture, sessionId);
4008
+ const layoutPayload = ensureCaptureSuccess(layoutCapture, sessionId);
4009
+ const properties = isRecord(stylePayload.properties) ? stylePayload.properties : {};
4010
+ const element = isRecord(layoutPayload.element) ? layoutPayload.element : {};
4011
+ const width = typeof element.width === 'number' ? element.width : 0;
4012
+ const height = typeof element.height === 'number' ? element.height : 0;
4013
+ const display = typeof properties.display === 'string' ? properties.display : undefined;
4014
+ const visibility = typeof properties.visibility === 'string' ? properties.visibility : undefined;
4015
+ const opacityText = typeof properties.opacity === 'string' ? properties.opacity : undefined;
4016
+ const opacity = opacityText !== undefined ? Number.parseFloat(opacityText) : undefined;
4017
+ const visible = display !== 'none'
4018
+ && visibility !== 'hidden'
4019
+ && visibility !== 'collapse'
4020
+ && opacity !== 0
4021
+ && width > 0
4022
+ && height > 0;
4023
+ return {
4024
+ selector,
4025
+ frameId,
4026
+ attached: true,
4027
+ visible,
4028
+ styles: properties,
4029
+ element,
4030
+ viewport: layoutPayload.viewport,
4031
+ };
4032
+ }
4033
+ catch (error) {
4034
+ if (isElementMissingError(error)) {
4035
+ return {
4036
+ selector,
4037
+ frameId,
4038
+ attached: false,
4039
+ visible: false,
4040
+ missing: true,
4041
+ message: error instanceof Error ? error.message : String(error),
4042
+ };
4043
+ }
4044
+ throw error;
4045
+ }
4046
+ }
4047
+ function selectorStateMatches(state, expectedState) {
4048
+ const attached = state.attached === true;
4049
+ const visible = state.visible === true;
4050
+ switch (expectedState) {
4051
+ case 'attached':
4052
+ return attached;
4053
+ case 'detached':
4054
+ return !attached;
4055
+ case 'visible':
4056
+ return attached && visible;
4057
+ case 'hidden':
4058
+ return !attached || !visible;
4059
+ }
4060
+ }
4061
+ async function waitForUrlCondition(sessionId, wait, capturePageState) {
4062
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4063
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4064
+ const startedAt = Date.now();
4065
+ const deadline = startedAt + timeoutMs;
4066
+ let attempts = 0;
4067
+ let lastPage;
4068
+ while (Date.now() <= deadline) {
4069
+ attempts += 1;
4070
+ const capture = await capturePageState(sessionId, {
4071
+ includeButtons: false,
4072
+ includeLinks: false,
4073
+ includeInputs: false,
4074
+ includeModals: false,
4075
+ maxItems: 1,
4076
+ maxTextLength: 40,
4077
+ });
4078
+ lastPage = {
4079
+ url: capture.payload.url,
4080
+ title: capture.payload.title,
4081
+ language: capture.payload.language,
4082
+ viewport: capture.payload.viewport,
4083
+ };
4084
+ if (matchesUrlWait(capture.payload.url, wait)) {
4085
+ return {
4086
+ waitKind: 'url',
4087
+ matched: true,
4088
+ waitedMs: Date.now() - startedAt,
4089
+ attempts,
4090
+ timeoutMs,
4091
+ pollIntervalMs,
4092
+ evidence: { page: lastPage },
4093
+ };
4094
+ }
4095
+ await sleep(pollIntervalMs);
4096
+ }
3046
4097
  return {
3047
- text: candidateTextForWorkflowTarget(item) || undefined,
3048
- testId: typeof item.testId === 'string' ? item.testId : undefined,
3049
- selector: typeof item.selector === 'string' ? item.selector : undefined,
3050
- tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
3051
- type: typeof item.type === 'string' ? item.type : undefined,
3052
- disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
3053
- selected: typeof item.selected === 'boolean' ? item.selected : undefined,
4098
+ waitKind: 'url',
4099
+ matched: false,
4100
+ waitedMs: Date.now() - startedAt,
4101
+ attempts,
4102
+ timeoutMs,
4103
+ pollIntervalMs,
4104
+ evidence: {
4105
+ page: lastPage,
4106
+ expected: wait,
4107
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4108
+ waitKind: 'url',
4109
+ timeoutMs,
4110
+ waitedMs: Date.now() - startedAt,
4111
+ attempts,
4112
+ pollIntervalMs,
4113
+ matcherSummary: {
4114
+ exactUrl: wait.exactUrl,
4115
+ urlContains: wait.urlContains,
4116
+ urlRegex: wait.urlRegex,
4117
+ },
4118
+ lastObserved: lastPage,
4119
+ }),
4120
+ },
4121
+ error: {
4122
+ code: 'url_wait_timeout',
4123
+ message: 'Timed out waiting for the page URL to match the requested condition.',
4124
+ },
3054
4125
  };
3055
4126
  }
3056
- function pickWorkflowTargetItems(payload, scope) {
3057
- if (scope) {
3058
- return pickPageStateScopeItems(payload, scope);
3059
- }
3060
- return [
3061
- ...pickPageStateScopeItems(payload, 'buttons'),
3062
- ...pickPageStateScopeItems(payload, 'inputs'),
3063
- ...pickPageStateScopeItems(payload, 'modals'),
3064
- ...pickPageStateScopeItems(payload, 'focused'),
3065
- ];
3066
- }
3067
- function matchesWorkflowActionTarget(item, target) {
3068
- return (equalsNormalized(item.testId, target.testId)
3069
- && includesNormalized(item.text, target.textContains)
3070
- && includesNormalized(item.label, target.labelContains)
3071
- && includesNormalized(item.title, target.titleContains)
3072
- && equalsNormalized(item.tagName, target.tagName)
3073
- && equalsNormalized(item.type, target.type)
3074
- && equalsOptionalBoolean(item.disabled, target.disabled)
3075
- && equalsOptionalBoolean(item.selected, target.selected)
3076
- && equalsOptionalBoolean(item.pressed, target.pressed)
3077
- && equalsOptionalBoolean(item.expanded, target.expanded)
3078
- && equalsOptionalBoolean(item.readOnly, target.readOnly)
3079
- && equalsOptionalBoolean(item.required, target.requiredField)
3080
- && (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
3081
- }
3082
- function summarizeWorkflowTargetMatcher(target) {
4127
+ function pageReadyStateMatches(readyState, expectedState) {
4128
+ if (readyState !== 'loading' && readyState !== 'interactive' && readyState !== 'complete') {
4129
+ return false;
4130
+ }
4131
+ if (expectedState === 'domcontentloaded') {
4132
+ return readyState === 'interactive' || readyState === 'complete';
4133
+ }
4134
+ return readyState === 'complete';
4135
+ }
4136
+ async function waitForLoadStateCondition(sessionId, wait, capturePageState) {
4137
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4138
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4139
+ const expectedState = wait.state ?? 'load';
4140
+ const startedAt = Date.now();
4141
+ const deadline = startedAt + timeoutMs;
4142
+ let attempts = 0;
4143
+ let lastPage;
4144
+ while (Date.now() <= deadline) {
4145
+ attempts += 1;
4146
+ const capture = await capturePageState(sessionId, {
4147
+ includeButtons: false,
4148
+ includeLinks: false,
4149
+ includeInputs: false,
4150
+ includeModals: false,
4151
+ maxItems: 1,
4152
+ maxTextLength: 40,
4153
+ });
4154
+ lastPage = {
4155
+ url: capture.payload.url,
4156
+ title: capture.payload.title,
4157
+ readyState: capture.payload.readyState,
4158
+ language: capture.payload.language,
4159
+ viewport: capture.payload.viewport,
4160
+ };
4161
+ const urlMatches = matchesUrlPredicates(typeof capture.payload.url === 'string' ? capture.payload.url : undefined, {
4162
+ exactUrl: wait.exactUrl,
4163
+ urlContains: wait.urlContains,
4164
+ urlRegex: wait.urlRegex,
4165
+ });
4166
+ if (urlMatches && pageReadyStateMatches(capture.payload.readyState, expectedState)) {
4167
+ return {
4168
+ waitKind: 'load_state',
4169
+ matched: true,
4170
+ waitedMs: Date.now() - startedAt,
4171
+ attempts,
4172
+ timeoutMs,
4173
+ pollIntervalMs,
4174
+ evidence: {
4175
+ state: expectedState,
4176
+ page: lastPage,
4177
+ },
4178
+ };
4179
+ }
4180
+ await sleep(pollIntervalMs);
4181
+ }
3083
4182
  return {
3084
- scope: target.scope,
3085
- selector: target.selector,
3086
- elementRef: target.elementRef,
3087
- testId: target.testId,
3088
- textContains: target.textContains,
3089
- labelContains: target.labelContains,
3090
- titleContains: target.titleContains,
3091
- tagName: target.tagName,
3092
- type: target.type,
3093
- disabled: target.disabled,
3094
- selected: target.selected,
3095
- pressed: target.pressed,
3096
- expanded: target.expanded,
3097
- readOnly: target.readOnly,
3098
- requiredField: target.requiredField,
4183
+ waitKind: 'load_state',
4184
+ matched: false,
4185
+ waitedMs: Date.now() - startedAt,
4186
+ attempts,
4187
+ timeoutMs,
4188
+ pollIntervalMs,
4189
+ evidence: {
4190
+ state: expectedState,
4191
+ page: lastPage,
4192
+ expected: wait,
4193
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4194
+ waitKind: 'load_state',
4195
+ timeoutMs,
4196
+ waitedMs: Date.now() - startedAt,
4197
+ attempts,
4198
+ pollIntervalMs,
4199
+ matcherSummary: {
4200
+ state: expectedState,
4201
+ exactUrl: wait.exactUrl,
4202
+ urlContains: wait.urlContains,
4203
+ urlRegex: wait.urlRegex,
4204
+ },
4205
+ lastObserved: lastPage,
4206
+ }),
4207
+ },
4208
+ error: {
4209
+ code: 'load_state_wait_timeout',
4210
+ message: `Timed out waiting for page load state "${expectedState}".`,
4211
+ },
3099
4212
  };
3100
4213
  }
3101
- async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
3102
- if (!target) {
3103
- return {
3104
- resolution: {
3105
- strategy: 'none',
4214
+ async function waitForSelectorStateCondition(sessionId, wait, captureClient) {
4215
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4216
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4217
+ const expectedState = wait.state ?? 'visible';
4218
+ const startedAt = Date.now();
4219
+ const deadline = startedAt + timeoutMs;
4220
+ let attempts = 0;
4221
+ let lastState;
4222
+ while (Date.now() <= deadline) {
4223
+ attempts += 1;
4224
+ lastState = await captureSelectorState(captureClient, sessionId, wait.selector, wait.frameId);
4225
+ if (selectorStateMatches(lastState, expectedState)) {
4226
+ return {
4227
+ waitKind: 'selector_state',
4228
+ matched: true,
4229
+ waitedMs: Date.now() - startedAt,
4230
+ attempts,
4231
+ timeoutMs,
4232
+ pollIntervalMs,
4233
+ evidence: {
4234
+ selector: wait.selector,
4235
+ state: expectedState,
4236
+ selectorState: lastState,
4237
+ },
4238
+ };
4239
+ }
4240
+ await sleep(pollIntervalMs);
4241
+ }
4242
+ return {
4243
+ waitKind: 'selector_state',
4244
+ matched: false,
4245
+ waitedMs: Date.now() - startedAt,
4246
+ attempts,
4247
+ timeoutMs,
4248
+ pollIntervalMs,
4249
+ evidence: {
4250
+ selector: wait.selector,
4251
+ state: expectedState,
4252
+ selectorState: lastState,
4253
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4254
+ waitKind: 'selector_state',
4255
+ timeoutMs,
4256
+ waitedMs: Date.now() - startedAt,
4257
+ attempts,
4258
+ pollIntervalMs,
4259
+ matcherSummary: {
4260
+ selector: wait.selector,
4261
+ state: expectedState,
4262
+ frameId: wait.frameId,
4263
+ },
4264
+ lastObserved: lastState,
4265
+ }),
4266
+ },
4267
+ error: {
4268
+ code: 'selector_state_wait_timeout',
4269
+ message: `Timed out waiting for selector "${wait.selector}" to become ${expectedState}.`,
4270
+ },
4271
+ };
4272
+ }
4273
+ async function waitForConsoleCondition(sessionId, wait, captureClient) {
4274
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4275
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4276
+ const levels = resolveLiveConsoleLevels(wait.levels);
4277
+ const contains = normalizeOptionalString(wait.contains);
4278
+ const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
4279
+ const includeRuntimeErrors = wait.includeRuntimeErrors !== false;
4280
+ const startedAt = Date.now();
4281
+ const deadline = startedAt + timeoutMs;
4282
+ let attempts = 0;
4283
+ let lastLogs = [];
4284
+ while (Date.now() <= deadline) {
4285
+ attempts += 1;
4286
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
4287
+ levels,
4288
+ contains,
4289
+ sinceTs,
4290
+ includeRuntimeErrors,
4291
+ limit: 10,
4292
+ }, 3_000);
4293
+ const payload = ensureCaptureSuccess(capture, sessionId);
4294
+ lastLogs = asRecordArray(payload.logs);
4295
+ if (lastLogs.length > 0) {
4296
+ return {
4297
+ waitKind: 'console',
4298
+ matched: true,
4299
+ waitedMs: Date.now() - startedAt,
4300
+ attempts,
4301
+ timeoutMs,
4302
+ pollIntervalMs,
4303
+ evidence: {
4304
+ filters: { levels, contains, sinceTs, includeRuntimeErrors },
4305
+ logs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4306
+ },
4307
+ };
4308
+ }
4309
+ await sleep(pollIntervalMs);
4310
+ }
4311
+ return {
4312
+ waitKind: 'console',
4313
+ matched: false,
4314
+ waitedMs: Date.now() - startedAt,
4315
+ attempts,
4316
+ timeoutMs,
4317
+ pollIntervalMs,
4318
+ evidence: {
4319
+ filters: { levels, contains, sinceTs, includeRuntimeErrors },
4320
+ sampledLogs: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4321
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4322
+ waitKind: 'console',
4323
+ timeoutMs,
4324
+ waitedMs: Date.now() - startedAt,
4325
+ attempts,
4326
+ pollIntervalMs,
4327
+ matcherSummary: { levels, contains, sinceTs, includeRuntimeErrors },
4328
+ lastObserved: lastLogs[0],
4329
+ candidateCount: lastLogs.length,
4330
+ sampledCandidates: lastLogs.slice(0, 5).map((log) => mapLiveConsoleLogRecord(log, 'compact')),
4331
+ }),
4332
+ },
4333
+ error: {
4334
+ code: 'console_wait_timeout',
4335
+ message: 'Timed out waiting for a matching live console log.',
4336
+ },
4337
+ };
4338
+ }
4339
+ async function waitForDialogCondition(sessionId, wait, captureClient) {
4340
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4341
+ const startedAt = Date.now();
4342
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DIALOG', {
4343
+ type: wait.type,
4344
+ messageContains: wait.messageContains,
4345
+ urlContains: wait.urlContains,
4346
+ action: wait.action,
4347
+ promptText: wait.promptText,
4348
+ tabId: wait.tabId,
4349
+ timeoutMs,
4350
+ }, timeoutMs + 2_000);
4351
+ const payload = ensureCaptureSuccess(capture, sessionId);
4352
+ const matched = payload.matched === true;
4353
+ return {
4354
+ waitKind: 'dialog',
4355
+ matched,
4356
+ waitedMs: Date.now() - startedAt,
4357
+ attempts: 1,
4358
+ timeoutMs,
4359
+ pollIntervalMs: timeoutMs,
4360
+ evidence: {
4361
+ filters: {
4362
+ type: wait.type,
4363
+ messageContains: wait.messageContains,
4364
+ urlContains: wait.urlContains,
4365
+ action: wait.action,
4366
+ tabId: wait.tabId,
3106
4367
  },
3107
- };
4368
+ dialog: matched ? payload : undefined,
4369
+ expected: matched ? undefined : payload.expected ?? wait,
4370
+ timeoutDiagnostics: matched
4371
+ ? undefined
4372
+ : buildWaitTimeoutDiagnostics({
4373
+ waitKind: 'dialog',
4374
+ timeoutMs,
4375
+ waitedMs: Date.now() - startedAt,
4376
+ attempts: 1,
4377
+ pollIntervalMs: timeoutMs,
4378
+ matcherSummary: {
4379
+ type: wait.type,
4380
+ messageContains: wait.messageContains,
4381
+ urlContains: wait.urlContains,
4382
+ action: wait.action,
4383
+ tabId: wait.tabId,
4384
+ },
4385
+ lastObserved: payload.lastObserved,
4386
+ candidateCount: typeof payload.observedCount === 'number' ? payload.observedCount : undefined,
4387
+ }),
4388
+ },
4389
+ error: matched
4390
+ ? undefined
4391
+ : {
4392
+ code: 'dialog_wait_timeout',
4393
+ message: 'Timed out waiting for a matching JavaScript dialog.',
4394
+ },
4395
+ };
4396
+ }
4397
+ async function waitForStableLayoutCondition(sessionId, wait, captureClient) {
4398
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4399
+ const stableMs = wait.stableMs;
4400
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4401
+ const startedAt = Date.now();
4402
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_STABLE_LAYOUT', {
4403
+ selector: wait.selector,
4404
+ stableMs,
4405
+ tabId: wait.tabId,
4406
+ timeoutMs,
4407
+ pollIntervalMs,
4408
+ }, timeoutMs + 2_000);
4409
+ const payload = ensureCaptureSuccess(capture, sessionId);
4410
+ const matched = payload.matched === true;
4411
+ return {
4412
+ waitKind: 'stable_layout',
4413
+ matched,
4414
+ waitedMs: Date.now() - startedAt,
4415
+ attempts: 1,
4416
+ timeoutMs,
4417
+ pollIntervalMs,
4418
+ evidence: {
4419
+ filters: {
4420
+ selector: wait.selector,
4421
+ stableMs,
4422
+ tabId: wait.tabId,
4423
+ },
4424
+ layout: payload,
4425
+ timeoutDiagnostics: matched
4426
+ ? undefined
4427
+ : buildWaitTimeoutDiagnostics({
4428
+ waitKind: 'stable_layout',
4429
+ timeoutMs,
4430
+ waitedMs: Date.now() - startedAt,
4431
+ attempts: typeof payload.attempts === 'number' ? payload.attempts : 1,
4432
+ pollIntervalMs,
4433
+ matcherSummary: {
4434
+ selector: wait.selector,
4435
+ stableMs,
4436
+ tabId: wait.tabId,
4437
+ },
4438
+ lastObserved: payload.snapshot,
4439
+ }),
4440
+ },
4441
+ error: matched
4442
+ ? undefined
4443
+ : {
4444
+ code: 'stable_layout_wait_timeout',
4445
+ message: 'Timed out waiting for layout to stay stable.',
4446
+ },
4447
+ };
4448
+ }
4449
+ function mapNavigationWaitEvent(row) {
4450
+ const payload = readJsonPayload(row.payload_json);
4451
+ return {
4452
+ eventId: row.event_id,
4453
+ sessionId: row.session_id,
4454
+ timestamp: row.ts,
4455
+ tabId: row.tab_id ?? undefined,
4456
+ origin: row.origin ?? undefined,
4457
+ url: resolveLastUrl(payload),
4458
+ from: typeof payload.from === 'string' ? payload.from : undefined,
4459
+ trigger: typeof payload.trigger === 'string' ? payload.trigger : undefined,
4460
+ payload,
4461
+ };
4462
+ }
4463
+ function navigationEventMatches(row, wait) {
4464
+ const payload = readJsonPayload(row.payload_json);
4465
+ const toUrl = resolveLastUrl(payload);
4466
+ const fromUrl = typeof payload.from === 'string' ? payload.from : undefined;
4467
+ const trigger = typeof payload.trigger === 'string' ? payload.trigger : undefined;
4468
+ if (!matchesUrlPredicates(toUrl, {
4469
+ exactUrl: wait.exactUrl,
4470
+ urlContains: wait.urlContains,
4471
+ urlRegex: wait.urlRegex,
4472
+ })) {
4473
+ return false;
3108
4474
  }
3109
- if (target.elementRef || target.selector) {
3110
- return {
3111
- target: {
3112
- elementRef: target.elementRef,
3113
- selector: target.selector,
3114
- tabId: target.tabId,
3115
- frameId: target.frameId,
3116
- url: target.url,
4475
+ if (wait.fromUrlContains || wait.fromUrlRegex) {
4476
+ if (!matchesUrlPredicates(fromUrl, {
4477
+ urlContains: wait.fromUrlContains,
4478
+ urlRegex: wait.fromUrlRegex,
4479
+ regexFieldName: 'fromUrlRegex',
4480
+ })) {
4481
+ return false;
4482
+ }
4483
+ }
4484
+ if (wait.trigger && trigger !== wait.trigger) {
4485
+ return false;
4486
+ }
4487
+ return true;
4488
+ }
4489
+ function queryNavigationWaitCandidates(db, options) {
4490
+ const where = ['session_id = ?', "type = 'nav'", 'ts >= ?'];
4491
+ const params = [options.sessionId, options.sinceTs];
4492
+ if (options.tabId !== undefined) {
4493
+ where.push('tab_id = ?');
4494
+ params.push(options.tabId);
4495
+ }
4496
+ return db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
4497
+ FROM events
4498
+ WHERE ${where.join(' AND ')}
4499
+ ORDER BY ts ASC
4500
+ LIMIT 200`).all(...params);
4501
+ }
4502
+ async function waitForNavigationCondition(sessionId, wait, db) {
4503
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4504
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, 100, 5_000);
4505
+ const sinceTs = resolveAutomationWaitSinceTs(wait.sinceTs);
4506
+ const tabId = resolveOptionalTabId(wait.tabId);
4507
+ const startedAt = Date.now();
4508
+ const deadline = startedAt + timeoutMs;
4509
+ let attempts = 0;
4510
+ let lastEvents = [];
4511
+ while (Date.now() <= deadline) {
4512
+ attempts += 1;
4513
+ lastEvents = queryNavigationWaitCandidates(db, { sessionId, sinceTs, tabId });
4514
+ const matched = lastEvents.find((row) => navigationEventMatches(row, wait));
4515
+ if (matched) {
4516
+ return {
4517
+ waitKind: 'navigation',
4518
+ matched: true,
4519
+ waitedMs: Date.now() - startedAt,
4520
+ attempts,
4521
+ timeoutMs,
4522
+ pollIntervalMs,
4523
+ evidence: {
4524
+ filters: {
4525
+ urlContains: wait.urlContains,
4526
+ urlRegex: wait.urlRegex,
4527
+ exactUrl: wait.exactUrl,
4528
+ fromUrlContains: wait.fromUrlContains,
4529
+ fromUrlRegex: wait.fromUrlRegex,
4530
+ trigger: wait.trigger,
4531
+ sinceTs,
4532
+ tabId,
4533
+ },
4534
+ navigation: mapNavigationWaitEvent(matched),
4535
+ },
4536
+ };
4537
+ }
4538
+ await sleep(pollIntervalMs);
4539
+ }
4540
+ return {
4541
+ waitKind: 'navigation',
4542
+ matched: false,
4543
+ waitedMs: Date.now() - startedAt,
4544
+ attempts,
4545
+ timeoutMs,
4546
+ pollIntervalMs,
4547
+ evidence: {
4548
+ filters: {
4549
+ urlContains: wait.urlContains,
4550
+ urlRegex: wait.urlRegex,
4551
+ exactUrl: wait.exactUrl,
4552
+ fromUrlContains: wait.fromUrlContains,
4553
+ fromUrlRegex: wait.fromUrlRegex,
4554
+ trigger: wait.trigger,
4555
+ sinceTs,
4556
+ tabId,
4557
+ },
4558
+ sampledEvents: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
4559
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4560
+ waitKind: 'navigation',
4561
+ timeoutMs,
4562
+ waitedMs: Date.now() - startedAt,
4563
+ attempts,
4564
+ pollIntervalMs,
4565
+ matcherSummary: {
4566
+ urlContains: wait.urlContains,
4567
+ urlRegex: wait.urlRegex,
4568
+ exactUrl: wait.exactUrl,
4569
+ fromUrlContains: wait.fromUrlContains,
4570
+ fromUrlRegex: wait.fromUrlRegex,
4571
+ trigger: wait.trigger,
4572
+ sinceTs,
4573
+ tabId,
4574
+ },
4575
+ lastObserved: lastEvents.length > 0 ? mapNavigationWaitEvent(lastEvents[lastEvents.length - 1]) : undefined,
4576
+ candidateCount: lastEvents.length,
4577
+ sampledCandidates: lastEvents.slice(0, 5).map((row) => mapNavigationWaitEvent(row)),
4578
+ }),
4579
+ },
4580
+ error: {
4581
+ code: 'navigation_wait_timeout',
4582
+ message: 'Timed out waiting for a matching navigation event.',
4583
+ },
4584
+ };
4585
+ }
4586
+ async function waitForNavigationLifecycleCondition(sessionId, wait, captureClient) {
4587
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4588
+ const startedAt = Date.now();
4589
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_NAVIGATION_LIFECYCLE', {
4590
+ state: wait.state,
4591
+ urlContains: wait.urlContains,
4592
+ urlRegex: wait.urlRegex,
4593
+ exactUrl: wait.exactUrl,
4594
+ tabId: wait.tabId,
4595
+ timeoutMs,
4596
+ }, timeoutMs + 2_000);
4597
+ const payload = ensureCaptureSuccess(capture, sessionId);
4598
+ const matched = payload.matched === true;
4599
+ return {
4600
+ waitKind: 'navigation_lifecycle',
4601
+ matched,
4602
+ waitedMs: Date.now() - startedAt,
4603
+ attempts: 1,
4604
+ timeoutMs,
4605
+ pollIntervalMs: timeoutMs,
4606
+ evidence: {
4607
+ filters: {
4608
+ state: wait.state,
4609
+ urlContains: wait.urlContains,
4610
+ urlRegex: wait.urlRegex,
4611
+ exactUrl: wait.exactUrl,
4612
+ tabId: wait.tabId,
4613
+ },
4614
+ lifecycle: matched ? payload : undefined,
4615
+ expected: matched ? undefined : payload.expected ?? wait,
4616
+ timeoutDiagnostics: matched
4617
+ ? undefined
4618
+ : buildWaitTimeoutDiagnostics({
4619
+ waitKind: 'navigation_lifecycle',
4620
+ timeoutMs,
4621
+ waitedMs: Date.now() - startedAt,
4622
+ attempts: 1,
4623
+ pollIntervalMs: timeoutMs,
4624
+ matcherSummary: {
4625
+ state: wait.state,
4626
+ urlContains: wait.urlContains,
4627
+ urlRegex: wait.urlRegex,
4628
+ exactUrl: wait.exactUrl,
4629
+ tabId: wait.tabId,
4630
+ },
4631
+ lastObserved: payload.lastObserved,
4632
+ candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
4633
+ }),
4634
+ },
4635
+ error: matched
4636
+ ? undefined
4637
+ : {
4638
+ code: 'navigation_lifecycle_wait_timeout',
4639
+ message: 'Timed out waiting for a matching navigation lifecycle event.',
4640
+ },
4641
+ };
4642
+ }
4643
+ async function waitForDownloadCondition(sessionId, wait, captureClient) {
4644
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4645
+ const startedAt = Date.now();
4646
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_DOWNLOAD', {
4647
+ urlContains: wait.urlContains,
4648
+ urlRegex: wait.urlRegex,
4649
+ exactUrl: wait.exactUrl,
4650
+ filenameContains: wait.filenameContains,
4651
+ filenameRegex: wait.filenameRegex,
4652
+ state: wait.state,
4653
+ tabId: wait.tabId,
4654
+ timeoutMs,
4655
+ }, timeoutMs + 2_000);
4656
+ const payload = ensureCaptureSuccess(capture, sessionId);
4657
+ const matched = payload.matched === true;
4658
+ return {
4659
+ waitKind: 'download',
4660
+ matched,
4661
+ waitedMs: Date.now() - startedAt,
4662
+ attempts: 1,
4663
+ timeoutMs,
4664
+ pollIntervalMs: timeoutMs,
4665
+ evidence: {
4666
+ filters: {
4667
+ urlContains: wait.urlContains,
4668
+ urlRegex: wait.urlRegex,
4669
+ exactUrl: wait.exactUrl,
4670
+ filenameContains: wait.filenameContains,
4671
+ filenameRegex: wait.filenameRegex,
4672
+ state: wait.state,
4673
+ tabId: wait.tabId,
3117
4674
  },
3118
- resolution: {
3119
- strategy: target.elementRef ? 'elementRef' : 'selector',
3120
- matcher: summarizeWorkflowTargetMatcher(target),
4675
+ download: matched ? payload : undefined,
4676
+ expected: matched ? undefined : payload.expected ?? wait,
4677
+ timeoutDiagnostics: matched
4678
+ ? undefined
4679
+ : buildWaitTimeoutDiagnostics({
4680
+ waitKind: 'download',
4681
+ timeoutMs,
4682
+ waitedMs: Date.now() - startedAt,
4683
+ attempts: 1,
4684
+ pollIntervalMs: timeoutMs,
4685
+ matcherSummary: {
4686
+ urlContains: wait.urlContains,
4687
+ urlRegex: wait.urlRegex,
4688
+ exactUrl: wait.exactUrl,
4689
+ filenameContains: wait.filenameContains,
4690
+ filenameRegex: wait.filenameRegex,
4691
+ state: wait.state,
4692
+ tabId: wait.tabId,
4693
+ },
4694
+ lastObserved: payload.lastObserved ?? payload.lastMatchedDownload,
4695
+ candidateCount: typeof payload.observedEventCount === 'number' ? payload.observedEventCount : undefined,
4696
+ }),
4697
+ },
4698
+ error: matched
4699
+ ? undefined
4700
+ : {
4701
+ code: 'download_wait_timeout',
4702
+ message: 'Timed out waiting for a matching download.',
3121
4703
  },
4704
+ };
4705
+ }
4706
+ async function waitForPopupCondition(sessionId, wait, captureClient) {
4707
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, 5_000, MAX_NETWORK_POLL_TIMEOUT_MS);
4708
+ const startedAt = Date.now();
4709
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_WAIT_FOR_POPUP', {
4710
+ urlContains: wait.urlContains,
4711
+ urlRegex: wait.urlRegex,
4712
+ exactUrl: wait.exactUrl,
4713
+ openerTabId: wait.openerTabId,
4714
+ timeoutMs,
4715
+ }, timeoutMs + 2_000);
4716
+ const payload = ensureCaptureSuccess(capture, sessionId);
4717
+ const matched = payload.matched === true;
4718
+ return {
4719
+ waitKind: 'popup',
4720
+ matched,
4721
+ waitedMs: Date.now() - startedAt,
4722
+ attempts: 1,
4723
+ timeoutMs,
4724
+ pollIntervalMs: timeoutMs,
4725
+ evidence: {
4726
+ filters: {
4727
+ urlContains: wait.urlContains,
4728
+ urlRegex: wait.urlRegex,
4729
+ exactUrl: wait.exactUrl,
4730
+ openerTabId: wait.openerTabId,
4731
+ },
4732
+ popup: matched ? payload : undefined,
4733
+ expected: matched ? undefined : payload.expected ?? wait,
4734
+ timeoutDiagnostics: matched
4735
+ ? undefined
4736
+ : buildWaitTimeoutDiagnostics({
4737
+ waitKind: 'popup',
4738
+ timeoutMs,
4739
+ waitedMs: Date.now() - startedAt,
4740
+ attempts: 1,
4741
+ pollIntervalMs: timeoutMs,
4742
+ matcherSummary: {
4743
+ urlContains: wait.urlContains,
4744
+ urlRegex: wait.urlRegex,
4745
+ exactUrl: wait.exactUrl,
4746
+ openerTabId: wait.openerTabId,
4747
+ },
4748
+ lastObserved: payload.lastObserved,
4749
+ candidateCount: typeof payload.observedPopupCount === 'number' ? payload.observedPopupCount : undefined,
4750
+ sampledCandidates: Array.isArray(payload.pendingTabIds) ? payload.pendingTabIds : undefined,
4751
+ }),
4752
+ },
4753
+ error: matched
4754
+ ? undefined
4755
+ : {
4756
+ code: 'popup_wait_timeout',
4757
+ message: 'Timed out waiting for a matching popup tab.',
4758
+ },
4759
+ };
4760
+ }
4761
+ function normalizeNetworkWaitFilters(wait) {
4762
+ const responseWait = wait.waitKind === 'response' ? wait : undefined;
4763
+ return {
4764
+ urlContains: normalizeOptionalString(wait.urlContains),
4765
+ urlRegex: normalizeOptionalString(wait.urlRegex),
4766
+ exactUrl: normalizeOptionalString(wait.exactUrl),
4767
+ method: normalizeHttpMethod(wait.method),
4768
+ traceId: normalizeOptionalString(wait.traceId),
4769
+ initiator: normalizeOptionalString(wait.initiator),
4770
+ requestContentType: normalizeOptionalString(wait.requestContentType),
4771
+ responseContentType: normalizeOptionalString(responseWait?.responseContentType),
4772
+ statusIn: responseWait ? normalizeStatusIn(responseWait.statusIn) : [],
4773
+ statusGte: responseWait?.statusGte,
4774
+ statusLt: responseWait?.statusLt,
4775
+ errorType: normalizeOptionalString(responseWait?.errorType),
4776
+ sinceTs: resolveAutomationWaitSinceTs(wait.sinceTs),
4777
+ tabId: resolveOptionalTabId(wait.tabId),
4778
+ includeBodies: wait.includeBodies === true,
4779
+ };
4780
+ }
4781
+ function queryNetworkWaitCandidates(db, sessionId, filters) {
4782
+ const where = ['session_id = ?', 'ts_start >= ?'];
4783
+ const params = [sessionId, filters.sinceTs];
4784
+ if (filters.exactUrl) {
4785
+ where.push('url = ?');
4786
+ params.push(filters.exactUrl);
4787
+ }
4788
+ else if (filters.urlContains) {
4789
+ where.push('url LIKE ?');
4790
+ params.push(`%${filters.urlContains}%`);
4791
+ }
4792
+ if (filters.method) {
4793
+ where.push('method = ?');
4794
+ params.push(filters.method);
4795
+ }
4796
+ if (filters.traceId) {
4797
+ where.push('trace_id = ?');
4798
+ params.push(filters.traceId);
4799
+ }
4800
+ if (filters.initiator) {
4801
+ where.push('initiator = ?');
4802
+ params.push(filters.initiator);
4803
+ }
4804
+ if (filters.requestContentType) {
4805
+ where.push('request_content_type LIKE ?');
4806
+ params.push(`%${filters.requestContentType}%`);
4807
+ }
4808
+ if (filters.responseContentType) {
4809
+ where.push('response_content_type LIKE ?');
4810
+ params.push(`%${filters.responseContentType}%`);
4811
+ }
4812
+ const statusIn = Array.isArray(filters.statusIn) ? filters.statusIn : [];
4813
+ if (statusIn.length > 0) {
4814
+ where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
4815
+ params.push(...statusIn);
4816
+ }
4817
+ if (typeof filters.statusGte === 'number') {
4818
+ where.push('status >= ?');
4819
+ params.push(filters.statusGte);
4820
+ }
4821
+ if (typeof filters.statusLt === 'number') {
4822
+ where.push('status < ?');
4823
+ params.push(filters.statusLt);
4824
+ }
4825
+ if (filters.tabId !== undefined) {
4826
+ where.push('tab_id = ?');
4827
+ params.push(filters.tabId);
4828
+ }
4829
+ return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
4830
+ FROM network
4831
+ WHERE ${where.join(' AND ')}
4832
+ ORDER BY ts_start ASC
4833
+ LIMIT 200`).all(...params);
4834
+ }
4835
+ function networkCallMatchesFilters(row, filters) {
4836
+ if (!matchesUrlPredicates(row.url, {
4837
+ exactUrl: typeof filters.exactUrl === 'string' ? filters.exactUrl : undefined,
4838
+ urlContains: typeof filters.urlContains === 'string' ? filters.urlContains : undefined,
4839
+ urlRegex: typeof filters.urlRegex === 'string' ? filters.urlRegex : undefined,
4840
+ })) {
4841
+ return false;
4842
+ }
4843
+ if (typeof filters.errorType === 'string' && classifyNetworkFailure(row.status, row.error_class) !== filters.errorType) {
4844
+ return false;
4845
+ }
4846
+ return true;
4847
+ }
4848
+ async function waitForNetworkMatchCondition(sessionId, wait, db) {
4849
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
4850
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
4851
+ const filters = normalizeNetworkWaitFilters(wait);
4852
+ const startedAt = Date.now();
4853
+ const deadline = startedAt + timeoutMs;
4854
+ let attempts = 0;
4855
+ let lastCalls = [];
4856
+ while (Date.now() <= deadline) {
4857
+ attempts += 1;
4858
+ lastCalls = queryNetworkWaitCandidates(db, sessionId, filters);
4859
+ const matched = lastCalls.find((row) => networkCallMatchesFilters(row, filters));
4860
+ if (matched) {
4861
+ return {
4862
+ waitKind: wait.waitKind,
4863
+ matched: true,
4864
+ waitedMs: Date.now() - startedAt,
4865
+ attempts,
4866
+ timeoutMs,
4867
+ pollIntervalMs,
4868
+ evidence: {
4869
+ filters,
4870
+ call: mapNetworkCallRecord(matched, filters.includeBodies === true),
4871
+ },
4872
+ };
4873
+ }
4874
+ await sleep(pollIntervalMs);
4875
+ }
4876
+ return {
4877
+ waitKind: wait.waitKind,
4878
+ matched: false,
4879
+ waitedMs: Date.now() - startedAt,
4880
+ attempts,
4881
+ timeoutMs,
4882
+ pollIntervalMs,
4883
+ evidence: {
4884
+ filters,
4885
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
4886
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4887
+ waitKind: wait.waitKind,
4888
+ timeoutMs,
4889
+ waitedMs: Date.now() - startedAt,
4890
+ attempts,
4891
+ pollIntervalMs,
4892
+ matcherSummary: filters,
4893
+ lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[lastCalls.length - 1], false) : undefined,
4894
+ candidateCount: lastCalls.length,
4895
+ sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
4896
+ }),
4897
+ },
4898
+ error: {
4899
+ code: wait.waitKind === 'request' ? 'request_wait_timeout' : 'response_wait_timeout',
4900
+ message: `Timed out waiting for a matching ${wait.waitKind}.`,
4901
+ },
4902
+ };
4903
+ }
4904
+ function queryRecentNetworkActivity(db, options) {
4905
+ const where = ['session_id = ?', 'ts_start >= ?'];
4906
+ const params = [options.sessionId, options.sinceTs];
4907
+ if (options.urlContains) {
4908
+ where.push('url LIKE ?');
4909
+ params.push(`%${options.urlContains}%`);
4910
+ }
4911
+ if (options.method) {
4912
+ where.push('method = ?');
4913
+ params.push(options.method);
4914
+ }
4915
+ if (options.tabId !== undefined) {
4916
+ where.push('tab_id = ?');
4917
+ params.push(options.tabId);
4918
+ }
4919
+ return db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
4920
+ FROM network
4921
+ WHERE ${where.join(' AND ')}
4922
+ ORDER BY ts_start DESC
4923
+ LIMIT 10`).all(...params);
4924
+ }
4925
+ async function waitForNetworkQuietCondition(sessionId, wait, db) {
4926
+ const timeoutMs = resolveTimeoutMs(wait.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
4927
+ const pollIntervalMs = resolveDurationMs(wait.pollIntervalMs, DEFAULT_NETWORK_POLL_INTERVAL_MS, 5_000);
4928
+ const quietMs = resolveDurationMs(wait.quietMs, 500, 10_000);
4929
+ const urlContains = normalizeOptionalString(wait.urlContains);
4930
+ const method = normalizeHttpMethod(wait.method);
4931
+ const tabId = resolveOptionalTabId(wait.tabId);
4932
+ const startedAt = Date.now();
4933
+ const deadline = startedAt + timeoutMs;
4934
+ let attempts = 0;
4935
+ let lastActivityAt = startedAt;
4936
+ let lastCalls = [];
4937
+ while (Date.now() <= deadline) {
4938
+ attempts += 1;
4939
+ const rows = queryRecentNetworkActivity(db, {
4940
+ sessionId,
4941
+ sinceTs: lastActivityAt + 1,
4942
+ urlContains,
4943
+ method,
4944
+ tabId,
4945
+ });
4946
+ if (rows.length > 0) {
4947
+ lastCalls = rows;
4948
+ lastActivityAt = Math.max(...rows.map((row) => row.ts_start), Date.now());
4949
+ }
4950
+ if (Date.now() - lastActivityAt >= quietMs) {
4951
+ return {
4952
+ waitKind: 'network_quiet',
4953
+ matched: true,
4954
+ waitedMs: Date.now() - startedAt,
4955
+ attempts,
4956
+ timeoutMs,
4957
+ pollIntervalMs,
4958
+ evidence: {
4959
+ quietMs,
4960
+ filters: { urlContains, method, tabId },
4961
+ lastActivityAt,
4962
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
4963
+ },
4964
+ };
4965
+ }
4966
+ await sleep(pollIntervalMs);
4967
+ }
4968
+ return {
4969
+ waitKind: 'network_quiet',
4970
+ matched: false,
4971
+ waitedMs: Date.now() - startedAt,
4972
+ attempts,
4973
+ timeoutMs,
4974
+ pollIntervalMs,
4975
+ evidence: {
4976
+ quietMs,
4977
+ filters: { urlContains, method, tabId },
4978
+ lastActivityAt,
4979
+ sampledCalls: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
4980
+ timeoutDiagnostics: buildWaitTimeoutDiagnostics({
4981
+ waitKind: 'network_quiet',
4982
+ timeoutMs,
4983
+ waitedMs: Date.now() - startedAt,
4984
+ attempts,
4985
+ pollIntervalMs,
4986
+ matcherSummary: { quietMs, urlContains, method, tabId },
4987
+ lastObserved: lastCalls.length > 0 ? mapNetworkCallRecord(lastCalls[0], false) : undefined,
4988
+ candidateCount: lastCalls.length,
4989
+ sampledCandidates: lastCalls.slice(0, 5).map((row) => mapNetworkCallRecord(row, false)),
4990
+ }),
4991
+ },
4992
+ error: {
4993
+ code: 'network_quiet_timeout',
4994
+ message: `Timed out waiting for ${quietMs}ms of quiet network activity.`,
4995
+ },
4996
+ };
4997
+ }
4998
+ async function runAutomationWait(options) {
4999
+ switch (options.wait.waitKind) {
5000
+ case 'url':
5001
+ return waitForUrlCondition(options.sessionId, options.wait, options.capturePageState);
5002
+ case 'navigation': {
5003
+ const db = options.getDb?.();
5004
+ if (!db) {
5005
+ throw new Error('navigation waits require database access');
5006
+ }
5007
+ return waitForNavigationCondition(options.sessionId, options.wait, db);
5008
+ }
5009
+ case 'navigation_lifecycle':
5010
+ return waitForNavigationLifecycleCondition(options.sessionId, options.wait, options.captureClient);
5011
+ case 'load_state':
5012
+ return waitForLoadStateCondition(options.sessionId, options.wait, options.capturePageState);
5013
+ case 'selector_state':
5014
+ return waitForSelectorStateCondition(options.sessionId, options.wait, options.captureClient);
5015
+ case 'console':
5016
+ return waitForConsoleCondition(options.sessionId, options.wait, options.captureClient);
5017
+ case 'dialog':
5018
+ return waitForDialogCondition(options.sessionId, options.wait, options.captureClient);
5019
+ case 'stable_layout':
5020
+ return waitForStableLayoutCondition(options.sessionId, options.wait, options.captureClient);
5021
+ case 'download':
5022
+ return waitForDownloadCondition(options.sessionId, options.wait, options.captureClient);
5023
+ case 'popup':
5024
+ return waitForPopupCondition(options.sessionId, options.wait, options.captureClient);
5025
+ case 'network_quiet': {
5026
+ const db = options.getDb?.();
5027
+ if (!db) {
5028
+ throw new Error('network_quiet waits require database access');
5029
+ }
5030
+ return waitForNetworkQuietCondition(options.sessionId, options.wait, db);
5031
+ }
5032
+ case 'request':
5033
+ case 'response': {
5034
+ const db = options.getDb?.();
5035
+ if (!db) {
5036
+ throw new Error(`${options.wait.waitKind} waits require database access`);
5037
+ }
5038
+ return waitForNetworkMatchCondition(options.sessionId, options.wait, db);
5039
+ }
5040
+ }
5041
+ }
5042
+ function getSessionRow(db, sessionId) {
5043
+ return db.prepare(`
5044
+ SELECT
5045
+ session_id,
5046
+ created_at,
5047
+ last_seen_at,
5048
+ paused_at,
5049
+ ended_at,
5050
+ tab_id,
5051
+ window_id,
5052
+ url_start,
5053
+ url_last,
5054
+ user_agent,
5055
+ viewport_w,
5056
+ viewport_h,
5057
+ dpr,
5058
+ safe_mode,
5059
+ pinned
5060
+ FROM sessions
5061
+ WHERE session_id = ?
5062
+ LIMIT 1
5063
+ `).get(sessionId);
5064
+ }
5065
+ function looksSensitiveText(value) {
5066
+ return typeof value === 'string'
5067
+ && /(password|passwd|pwd|token|secret|auth|session|email|card|cvv|cvc|ssn|iban|payment|billing)/i.test(value);
5068
+ }
5069
+ function isSensitivePageInput(input) {
5070
+ const type = typeof input.type === 'string' ? input.type.toLowerCase() : '';
5071
+ return type === 'password'
5072
+ || looksSensitiveText(input.selector)
5073
+ || looksSensitiveText(input.label)
5074
+ || looksSensitiveText(input.name)
5075
+ || looksSensitiveText(input.placeholder)
5076
+ || looksSensitiveText(input.testId);
5077
+ }
5078
+ function collectAutomationPageRisks(payload) {
5079
+ if (!payload) {
5080
+ return {
5081
+ sensitiveInputs: [],
5082
+ frameCount: 0,
5083
+ crossOriginFrameCount: 0,
3122
5084
  };
3123
5085
  }
3124
- const capture = existingCapture ?? await capturePageState(sessionId, {
3125
- includeButtons: target.scope ? target.scope === 'buttons' : true,
3126
- includeInputs: target.scope ? target.scope === 'inputs' : true,
3127
- includeModals: target.scope ? target.scope === 'modals' : true,
3128
- maxItems: 100,
3129
- maxTextLength: 120,
3130
- });
3131
- const candidates = pickWorkflowTargetItems(capture.payload, target.scope)
3132
- .filter((item) => matchesWorkflowActionTarget(item, target));
3133
- if (candidates.length === 0) {
3134
- throw new WorkflowTargetResolutionError('workflow_target_not_found', 'No interactive element matched the workflow target.', {
3135
- matcher: summarizeWorkflowTargetMatcher(target),
3136
- searchedScope: target.scope ?? 'all-interactive',
3137
- sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
3138
- .slice(0, 5)
3139
- .map((item) => describeWorkflowTargetCandidate(item)),
5086
+ const inputs = asRecordArray(payload.inputs);
5087
+ const frames = asRecordArray(payload.frames);
5088
+ const sensitiveInputs = inputs
5089
+ .filter(isSensitivePageInput)
5090
+ .slice(0, 8)
5091
+ .map((input) => ({
5092
+ selector: input.selector,
5093
+ type: input.type,
5094
+ label: input.label,
5095
+ name: input.name,
5096
+ placeholder: input.placeholder,
5097
+ frameId: input.frameId,
5098
+ frameUrl: input.frameUrl,
5099
+ }));
5100
+ const crossOriginFrames = frames
5101
+ .filter((frame) => frame.sameOrigin === false || frame.accessible === false || frame.crossOrigin === true)
5102
+ .slice(0, 8)
5103
+ .map((frame) => ({
5104
+ frameId: frame.frameId,
5105
+ url: frame.url ?? frame.frameUrl,
5106
+ title: frame.title ?? frame.frameTitle,
5107
+ sameOrigin: frame.sameOrigin,
5108
+ accessible: frame.accessible,
5109
+ }));
5110
+ return {
5111
+ sensitiveInputs,
5112
+ sensitiveInputCount: sensitiveInputs.length,
5113
+ frameCount: frames.length,
5114
+ crossOriginFrameCount: crossOriginFrames.length,
5115
+ crossOriginFrames,
5116
+ };
5117
+ }
5118
+ async function buildAutomationFlowPreflight(options) {
5119
+ const blockers = [];
5120
+ const warnings = [];
5121
+ const includePageState = options.input.includePageState !== false;
5122
+ const expectedUrlContains = normalizeOptionalString(options.input.expectedUrlContains);
5123
+ const requireSensitiveAutomation = options.input.requireSensitiveAutomation === true;
5124
+ const plannedActions = Array.isArray(options.input.plannedActions)
5125
+ ? options.input.plannedActions.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
5126
+ : [];
5127
+ const db = options.getDb?.();
5128
+ const session = db ? getSessionRow(db, options.sessionId) : undefined;
5129
+ const sessionState = options.getSessionConnectionState?.(options.sessionId);
5130
+ const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
5131
+ const scope = classifySessionUrl(session?.url_last ?? undefined);
5132
+ const liveConnection = session
5133
+ ? buildLiveConnectionRecord(session, scope, sessionState)
5134
+ : {
5135
+ connected: sessionState?.connected === true,
5136
+ status: sessionState?.connected === true ? 'connected' : 'unknown',
5137
+ recommendedForLiveCapture: false,
5138
+ };
5139
+ if (!db) {
5140
+ warnings.push({
5141
+ code: 'DB_UNAVAILABLE',
5142
+ severity: 'warning',
5143
+ source: 'server',
5144
+ message: 'Database access was not available; session history checks were skipped.',
3140
5145
  });
3141
5146
  }
3142
- if (candidates.length > 1) {
3143
- throw new WorkflowTargetResolutionError('workflow_target_ambiguous', `Workflow target matched ${candidates.length} elements; refine the matcher.`, {
3144
- matcher: summarizeWorkflowTargetMatcher(target),
3145
- matchedCandidateCount: candidates.length,
3146
- sampledCandidates: candidates.slice(0, 5).map((item) => describeWorkflowTargetCandidate(item)),
5147
+ if (!session) {
5148
+ blockers.push({
5149
+ code: 'SESSION_NOT_FOUND',
5150
+ severity: 'error',
5151
+ source: 'session',
5152
+ message: `Session not found: ${options.sessionId}`,
3147
5153
  });
3148
5154
  }
3149
- const candidate = candidates[0];
5155
+ else {
5156
+ const status = getSessionStatus(session);
5157
+ if (status === 'paused') {
5158
+ blockers.push({
5159
+ code: 'SESSION_PAUSED',
5160
+ severity: 'error',
5161
+ source: 'session',
5162
+ message: 'Resume the session before running an automation flow.',
5163
+ });
5164
+ }
5165
+ if (status === 'ended') {
5166
+ blockers.push({
5167
+ code: 'SESSION_ENDED',
5168
+ severity: 'error',
5169
+ source: 'session',
5170
+ message: 'Start a new session before running an automation flow.',
5171
+ });
5172
+ }
5173
+ if (scope.kind === 'likely_iframe_noise') {
5174
+ blockers.push({
5175
+ code: 'SESSION_SCOPE_NOISE',
5176
+ severity: 'error',
5177
+ source: 'session',
5178
+ message: 'The selected session appears to be bound to iframe/ad traffic rather than the app surface.',
5179
+ });
5180
+ }
5181
+ if (scope.kind === 'top_level_page' && scope.isLocalhost !== true) {
5182
+ warnings.push({
5183
+ code: 'PRODUCTION_OR_REMOTE_ORIGIN',
5184
+ severity: 'warning',
5185
+ source: 'session',
5186
+ message: 'The current session URL is remote/production-like. Keep the flow scoped and avoid destructive actions.',
5187
+ origin: scope.origin,
5188
+ });
5189
+ }
5190
+ if (expectedUrlContains && !String(session.url_last ?? '').includes(expectedUrlContains)) {
5191
+ blockers.push({
5192
+ code: 'EXPECTED_URL_MISMATCH',
5193
+ severity: 'error',
5194
+ source: 'session',
5195
+ message: `Current session URL does not include "${expectedUrlContains}".`,
5196
+ currentUrl: session.url_last,
5197
+ });
5198
+ }
5199
+ }
5200
+ if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
5201
+ blockers.push({
5202
+ code: LIVE_SESSION_DISCONNECTED_CODE,
5203
+ severity: 'error',
5204
+ source: 'connection',
5205
+ message: 'The session is not currently connected to a live extension target.',
5206
+ disconnectedAt: sessionState?.disconnectedAt,
5207
+ disconnectReason: sessionState?.disconnectReason,
5208
+ });
5209
+ }
5210
+ let pageCapture;
5211
+ if (includePageState && blockers.length === 0) {
5212
+ try {
5213
+ pageCapture = await options.capturePageState(options.sessionId, {
5214
+ includeButtons: true,
5215
+ includeLinks: true,
5216
+ includeInputs: true,
5217
+ includeModals: true,
5218
+ maxItems: resolveLimit(options.input.maxItems, 40),
5219
+ maxTextLength: resolveDurationMs(options.input.maxTextLength, 80, 200),
5220
+ });
5221
+ }
5222
+ catch (error) {
5223
+ blockers.push({
5224
+ code: isLiveSessionDisconnectedError(error) ? LIVE_SESSION_DISCONNECTED_CODE : 'PAGE_STATE_CAPTURE_FAILED',
5225
+ severity: 'error',
5226
+ source: 'page-state',
5227
+ message: error instanceof Error ? error.message : String(error),
5228
+ });
5229
+ }
5230
+ }
5231
+ const pageRisks = collectAutomationPageRisks(pageCapture?.payload);
5232
+ const sensitiveInputs = Array.isArray(pageRisks.sensitiveInputs) ? pageRisks.sensitiveInputs : [];
5233
+ const hasInputLikeAction = plannedActions.some((action) => ['input', 'type', 'clear', 'select_option', 'press_key'].includes(action));
5234
+ if (sensitiveInputs.length > 0 && (requireSensitiveAutomation || hasInputLikeAction)) {
5235
+ warnings.push({
5236
+ code: 'SENSITIVE_FIELD_AUTOMATION_RISK',
5237
+ severity: 'warning',
5238
+ source: 'page-state',
5239
+ message: 'Sensitive-looking fields are present. The extension sensitive-field opt-in may be required before input-like actions.',
5240
+ count: sensitiveInputs.length,
5241
+ sampledInputs: sensitiveInputs,
5242
+ });
5243
+ }
5244
+ if (typeof pageRisks.crossOriginFrameCount === 'number' && pageRisks.crossOriginFrameCount > 0) {
5245
+ warnings.push({
5246
+ code: 'CROSS_ORIGIN_FRAME_PRESENT',
5247
+ severity: 'warning',
5248
+ source: 'page-state',
5249
+ message: 'Cross-origin or inaccessible frames are present. Automation inside those frames may be diagnostic-only.',
5250
+ count: pageRisks.crossOriginFrameCount,
5251
+ frames: pageRisks.crossOriginFrames,
5252
+ });
5253
+ }
5254
+ const ready = blockers.length === 0;
3150
5255
  return {
3151
- target: {
3152
- elementRef: typeof candidate.elementRef === 'string' ? candidate.elementRef : undefined,
3153
- selector: typeof candidate.selector === 'string' ? candidate.selector : undefined,
3154
- tabId: target.tabId,
3155
- frameId: target.frameId,
3156
- url: target.url,
3157
- },
3158
- resolution: {
3159
- strategy: typeof candidate.elementRef === 'string' ? 'semantic_elementRef' : 'semantic_selector',
3160
- matcher: summarizeWorkflowTargetMatcher(target),
3161
- matchedCandidateCount: candidates.length,
3162
- matched: describeWorkflowTargetCandidate(candidate),
3163
- },
3164
- pageCapture: capture,
5256
+ ready,
5257
+ blockers,
5258
+ warnings,
5259
+ checks: {
5260
+ sessionFound: Boolean(session),
5261
+ liveConnected: sessionState?.connected === true || (hasLiveConnectionLookup ? false : undefined),
5262
+ recommendedForLiveCapture: liveConnection.recommendedForLiveCapture,
5263
+ expectedUrlMatched: expectedUrlContains ? blockers.every((blocker) => blocker.code !== 'EXPECTED_URL_MISMATCH') : undefined,
5264
+ pageStateCaptured: pageCapture !== undefined,
5265
+ remoteOrProductionLike: scope.kind === 'top_level_page' && scope.isLocalhost !== true,
5266
+ sensitiveInputCount: sensitiveInputs.length,
5267
+ crossOriginFrameCount: pageRisks.crossOriginFrameCount,
5268
+ },
5269
+ session: session
5270
+ ? {
5271
+ sessionId: session.session_id,
5272
+ status: getSessionStatus(session),
5273
+ tabId: session.tab_id ?? undefined,
5274
+ windowId: session.window_id ?? undefined,
5275
+ urlStart: session.url_start ?? undefined,
5276
+ urlLast: session.url_last ?? undefined,
5277
+ lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
5278
+ safeMode: session.safe_mode === 1,
5279
+ }
5280
+ : undefined,
5281
+ scope,
5282
+ liveConnection,
5283
+ page: pageCapture
5284
+ ? {
5285
+ url: pageCapture.payload.url,
5286
+ title: pageCapture.payload.title,
5287
+ language: pageCapture.payload.language,
5288
+ viewport: pageCapture.payload.viewport,
5289
+ summary: pageCapture.payload.summary,
5290
+ }
5291
+ : undefined,
5292
+ detectedRisks: pageRisks,
5293
+ nextActions: ready
5294
+ ? [{ code: 'RUN_FLOW', message: 'Run the automation flow with bounded waits and failure capture enabled.' }]
5295
+ : blockers.map((blocker) => ({
5296
+ code: String(blocker.code ?? 'FIX_BLOCKER'),
5297
+ message: String(blocker.message ?? 'Resolve this preflight blocker before running the flow.'),
5298
+ })),
3165
5299
  };
3166
5300
  }
3167
5301
  function createWorkflowStepId(step, index) {
@@ -3172,6 +5306,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
3172
5306
  const maxTextLength = mode === 'fast' ? 60 : 80;
3173
5307
  return capturePageState(sessionId, {
3174
5308
  includeButtons: true,
5309
+ includeLinks: true,
3175
5310
  includeInputs: true,
3176
5311
  includeModals: true,
3177
5312
  maxItems,
@@ -3226,6 +5361,20 @@ function resolveWorkflowRecommendedAction(error) {
3226
5361
  if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
3227
5362
  return 'inspect_page_state';
3228
5363
  }
5364
+ if (error.code === 'url_wait_timeout' || error.code === 'navigation_wait_timeout') {
5365
+ return 'inspect_navigation_state';
5366
+ }
5367
+ if (error.code === 'selector_state_wait_timeout') {
5368
+ return 'inspect_selector_state';
5369
+ }
5370
+ if (error.code === 'console_wait_timeout') {
5371
+ return 'inspect_live_console_logs';
5372
+ }
5373
+ if (error.code === 'network_quiet_timeout'
5374
+ || error.code === 'request_wait_timeout'
5375
+ || error.code === 'response_wait_timeout') {
5376
+ return 'inspect_network_calls';
5377
+ }
3229
5378
  return undefined;
3230
5379
  }
3231
5380
  function resolveWorkflowFailureSelector(step, stepResultTarget) {
@@ -3970,15 +6119,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3970
6119
  recommendedAction,
3971
6120
  };
3972
6121
  },
3973
- list_override_profiles: async () => {
6122
+ list_override_profiles: async (input) => {
3974
6123
  const profiles = buildOverrideProfileRecords();
6124
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
3975
6125
  return {
3976
6126
  ...createBaseResponse(),
3977
6127
  limitsApplied: {
3978
6128
  maxResults: profiles.length,
3979
6129
  truncated: false,
3980
6130
  },
3981
- profiles,
6131
+ responseProfile,
6132
+ profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
3982
6133
  nextActions: profiles.length > 0
3983
6134
  ? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
3984
6135
  : [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
@@ -4016,6 +6167,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4016
6167
  });
4017
6168
  const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
4018
6169
  const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
6170
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
6171
+ const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
4019
6172
  const write = {
4020
6173
  written: false,
4021
6174
  path: generated.suggestedConfigPath,
@@ -4064,21 +6217,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4064
6217
  warnings: generated.warnings,
4065
6218
  nextActions,
4066
6219
  write,
4067
- profile: generated.profile,
4068
- config: generated.config,
4069
- configJson: generated.configJson,
6220
+ responseProfile,
6221
+ profile: responseProfile === 'full' ? generated.profile : compactOverrideProfile(generated.profile),
6222
+ config: responseProfile === 'full'
6223
+ ? generated.config
6224
+ : {
6225
+ enabled: generated.config.enabled,
6226
+ activeProfileId: generated.config.activeProfileId,
6227
+ profileCount: generated.config.profiles.length,
6228
+ },
6229
+ configJson: includeConfigJson ? generated.configJson : undefined,
6230
+ configJsonOmitted: !includeConfigJson,
4070
6231
  };
4071
6232
  },
4072
6233
  validate_override_profile: async (input) => {
4073
6234
  const profile = resolveOverrideProfileRecord(input.profileId);
4074
6235
  const issues = buildOverrideProfileIssues(profile);
6236
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
4075
6237
  return {
4076
6238
  ...createBaseResponse(),
4077
6239
  profileId: profile.profileId,
4078
6240
  valid: !issues.some((issue) => issue.severity === 'error'),
4079
6241
  issues,
4080
6242
  nextActions: buildOverrideProfileNextActions(profile, issues),
4081
- profile,
6243
+ responseProfile,
6244
+ profile: serializeOverrideProfile(profile, responseProfile),
4082
6245
  };
4083
6246
  },
4084
6247
  preflight_overrides: async (input) => {
@@ -4105,16 +6268,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
4105
6268
  }
4106
6269
  const assets = listObservedOverrideAssets(getDb(), {
4107
6270
  sessionId,
4108
- limit: typeof input.limit === 'number' ? input.limit : undefined,
6271
+ limit: typeof input.limit === 'number' ? input.limit : 50,
4109
6272
  sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
4110
6273
  });
6274
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
4111
6275
  return {
4112
6276
  ...createBaseResponse(sessionId),
4113
6277
  limitsApplied: {
4114
6278
  maxResults: assets.length,
4115
6279
  truncated: false,
4116
6280
  },
4117
- assets,
6281
+ responseProfile,
6282
+ assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
4118
6283
  };
4119
6284
  },
4120
6285
  plan_override_response_patch: async (input) => {
@@ -5562,9 +7727,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5562
7727
  r.status,
5563
7728
  r.started_at,
5564
7729
  r.completed_at,
5565
- r.stop_reason,
5566
- r.target_summary_json,
5567
- r.failure_json,
7730
+ r.stop_reason,
7731
+ r.target_summary_json,
7732
+ r.diagnostics_json,
7733
+ r.failure_json,
5568
7734
  r.redaction_json,
5569
7735
  r.created_at,
5570
7736
  r.updated_at,
@@ -5626,9 +7792,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5626
7792
  r.status,
5627
7793
  r.started_at,
5628
7794
  r.completed_at,
5629
- r.stop_reason,
5630
- r.target_summary_json,
5631
- r.failure_json,
7795
+ r.stop_reason,
7796
+ r.target_summary_json,
7797
+ r.diagnostics_json,
7798
+ r.failure_json,
5632
7799
  r.redaction_json,
5633
7800
  r.created_at,
5634
7801
  r.updated_at,
@@ -5660,9 +7827,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5660
7827
  started_at,
5661
7828
  finished_at,
5662
7829
  duration_ms,
5663
- tab_id,
5664
- target_summary_json,
5665
- redaction_json,
7830
+ tab_id,
7831
+ target_summary_json,
7832
+ diagnostics_json,
7833
+ redaction_json,
5666
7834
  failure_json,
5667
7835
  input_metadata_json,
5668
7836
  event_type,
@@ -5696,12 +7864,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5696
7864
  const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
5697
7865
  const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
5698
7866
  const includeButtons = input.includeButtons !== false;
7867
+ const includeLinks = input.includeLinks !== false;
5699
7868
  const includeInputs = input.includeInputs !== false;
5700
7869
  const includeModals = input.includeModals !== false;
5701
7870
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
5702
7871
  maxItems,
5703
7872
  maxTextLength,
5704
7873
  includeButtons,
7874
+ includeLinks,
5705
7875
  includeInputs,
5706
7876
  includeModals,
5707
7877
  }, 4_000);
@@ -6376,7 +8546,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6376
8546
  throw new Error('selector is required');
6377
8547
  }
6378
8548
  const properties = asStringArray(input.properties, 64);
6379
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
8549
+ const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
8550
+ ? Math.max(0, Math.floor(input.frameId))
8551
+ : 0;
8552
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, frameId, properties }, 3_000);
6380
8553
  return {
6381
8554
  ...createBaseResponse(sessionId),
6382
8555
  limitsApplied: {
@@ -6392,7 +8565,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6392
8565
  throw new Error('sessionId is required');
6393
8566
  }
6394
8567
  const selector = typeof input.selector === 'string' ? input.selector : undefined;
6395
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
8568
+ const frameId = typeof input.frameId === 'number' && Number.isFinite(input.frameId)
8569
+ ? Math.max(0, Math.floor(input.frameId))
8570
+ : 0;
8571
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector, frameId }, 3_000);
6396
8572
  return {
6397
8573
  ...createBaseResponse(sessionId),
6398
8574
  limitsApplied: {
@@ -6423,6 +8599,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6423
8599
  const normalizedInput = {
6424
8600
  ...input,
6425
8601
  includeButtons: kinds.includes('buttons'),
8602
+ includeLinks: kinds.includes('links'),
6426
8603
  includeInputs: kinds.includes('inputs'),
6427
8604
  includeModals: kinds.includes('modals'),
6428
8605
  };
@@ -6503,6 +8680,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6503
8680
  ...waited,
6504
8681
  };
6505
8682
  },
8683
+ preflight_automation_flow: async (input) => {
8684
+ const sessionId = getSessionId(input);
8685
+ if (!sessionId) {
8686
+ throw new Error('sessionId is required');
8687
+ }
8688
+ const preflight = await buildAutomationFlowPreflight({
8689
+ sessionId,
8690
+ input,
8691
+ capturePageState,
8692
+ getDb,
8693
+ getSessionConnectionState,
8694
+ });
8695
+ return {
8696
+ ...createBaseResponse(sessionId),
8697
+ limitsApplied: {
8698
+ maxResults: 1,
8699
+ truncated: false,
8700
+ },
8701
+ ...preflight,
8702
+ };
8703
+ },
8704
+ wait_for_url: async (input) => {
8705
+ const sessionId = getSessionId(input);
8706
+ if (!sessionId) {
8707
+ throw new Error('sessionId is required');
8708
+ }
8709
+ const wait = AutomationWaitUrlSchema.parse({ ...input, waitKind: 'url' });
8710
+ const waited = await waitForUrlCondition(sessionId, wait, capturePageState);
8711
+ return {
8712
+ ...createBaseResponse(sessionId),
8713
+ limitsApplied: {
8714
+ maxResults: 1,
8715
+ truncated: false,
8716
+ },
8717
+ ...waited,
8718
+ };
8719
+ },
8720
+ wait_for_navigation: async (input) => {
8721
+ const sessionId = getSessionId(input);
8722
+ if (!sessionId) {
8723
+ throw new Error('sessionId is required');
8724
+ }
8725
+ if (!getDb) {
8726
+ throw new Error('wait_for_navigation requires database access');
8727
+ }
8728
+ const wait = AutomationWaitNavigationSchema.parse({ ...input, waitKind: 'navigation' });
8729
+ const waited = await waitForNavigationCondition(sessionId, wait, getDb());
8730
+ return {
8731
+ ...createBaseResponse(sessionId),
8732
+ limitsApplied: {
8733
+ maxResults: 10,
8734
+ truncated: false,
8735
+ },
8736
+ ...waited,
8737
+ };
8738
+ },
8739
+ wait_for_navigation_lifecycle: async (input) => {
8740
+ const sessionId = getSessionId(input);
8741
+ if (!sessionId) {
8742
+ throw new Error('sessionId is required');
8743
+ }
8744
+ const wait = AutomationWaitNavigationLifecycleSchema.parse({ ...input, waitKind: 'navigation_lifecycle' });
8745
+ const waited = await waitForNavigationLifecycleCondition(sessionId, wait, captureClient);
8746
+ return {
8747
+ ...createBaseResponse(sessionId),
8748
+ limitsApplied: {
8749
+ maxResults: 1,
8750
+ truncated: false,
8751
+ },
8752
+ ...waited,
8753
+ };
8754
+ },
8755
+ wait_for_load_state: async (input) => {
8756
+ const sessionId = getSessionId(input);
8757
+ if (!sessionId) {
8758
+ throw new Error('sessionId is required');
8759
+ }
8760
+ const wait = AutomationWaitLoadStateSchema.parse({ ...input, waitKind: 'load_state' });
8761
+ const waited = await waitForLoadStateCondition(sessionId, wait, capturePageState);
8762
+ return {
8763
+ ...createBaseResponse(sessionId),
8764
+ limitsApplied: {
8765
+ maxResults: 1,
8766
+ truncated: false,
8767
+ },
8768
+ ...waited,
8769
+ };
8770
+ },
8771
+ wait_for_selector_state: async (input) => {
8772
+ const sessionId = getSessionId(input);
8773
+ if (!sessionId) {
8774
+ throw new Error('sessionId is required');
8775
+ }
8776
+ const wait = AutomationWaitSelectorStateSchema.parse({ ...input, waitKind: 'selector_state' });
8777
+ const waited = await waitForSelectorStateCondition(sessionId, wait, captureClient);
8778
+ return {
8779
+ ...createBaseResponse(sessionId),
8780
+ limitsApplied: {
8781
+ maxResults: 1,
8782
+ truncated: false,
8783
+ },
8784
+ ...waited,
8785
+ };
8786
+ },
8787
+ wait_for_request: async (input) => {
8788
+ const sessionId = getSessionId(input);
8789
+ if (!sessionId) {
8790
+ throw new Error('sessionId is required');
8791
+ }
8792
+ if (!getDb) {
8793
+ throw new Error('wait_for_request requires database access');
8794
+ }
8795
+ const wait = AutomationWaitRequestSchema.parse({ ...input, waitKind: 'request' });
8796
+ const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
8797
+ return {
8798
+ ...createBaseResponse(sessionId),
8799
+ limitsApplied: {
8800
+ maxResults: 10,
8801
+ truncated: false,
8802
+ },
8803
+ ...waited,
8804
+ };
8805
+ },
8806
+ wait_for_response: async (input) => {
8807
+ const sessionId = getSessionId(input);
8808
+ if (!sessionId) {
8809
+ throw new Error('sessionId is required');
8810
+ }
8811
+ if (!getDb) {
8812
+ throw new Error('wait_for_response requires database access');
8813
+ }
8814
+ const wait = AutomationWaitResponseSchema.parse({ ...input, waitKind: 'response' });
8815
+ const waited = await waitForNetworkMatchCondition(sessionId, wait, getDb());
8816
+ return {
8817
+ ...createBaseResponse(sessionId),
8818
+ limitsApplied: {
8819
+ maxResults: 10,
8820
+ truncated: false,
8821
+ },
8822
+ ...waited,
8823
+ };
8824
+ },
8825
+ wait_for_console: async (input) => {
8826
+ const sessionId = getSessionId(input);
8827
+ if (!sessionId) {
8828
+ throw new Error('sessionId is required');
8829
+ }
8830
+ const wait = AutomationWaitConsoleSchema.parse({ ...input, waitKind: 'console' });
8831
+ const waited = await waitForConsoleCondition(sessionId, wait, captureClient);
8832
+ return {
8833
+ ...createBaseResponse(sessionId),
8834
+ limitsApplied: {
8835
+ maxResults: 10,
8836
+ truncated: false,
8837
+ },
8838
+ ...waited,
8839
+ };
8840
+ },
8841
+ wait_for_dialog: async (input) => {
8842
+ const sessionId = getSessionId(input);
8843
+ if (!sessionId) {
8844
+ throw new Error('sessionId is required');
8845
+ }
8846
+ const wait = AutomationWaitDialogSchema.parse({ ...input, waitKind: 'dialog' });
8847
+ const waited = await waitForDialogCondition(sessionId, wait, captureClient);
8848
+ return {
8849
+ ...createBaseResponse(sessionId),
8850
+ limitsApplied: {
8851
+ maxResults: 1,
8852
+ truncated: false,
8853
+ },
8854
+ ...waited,
8855
+ };
8856
+ },
8857
+ wait_for_stable_layout: async (input) => {
8858
+ const sessionId = getSessionId(input);
8859
+ if (!sessionId) {
8860
+ throw new Error('sessionId is required');
8861
+ }
8862
+ const wait = AutomationWaitStableLayoutSchema.parse({ ...input, waitKind: 'stable_layout' });
8863
+ const waited = await waitForStableLayoutCondition(sessionId, wait, captureClient);
8864
+ return {
8865
+ ...createBaseResponse(sessionId),
8866
+ limitsApplied: {
8867
+ maxResults: 1,
8868
+ truncated: false,
8869
+ },
8870
+ ...waited,
8871
+ };
8872
+ },
8873
+ wait_for_download: async (input) => {
8874
+ const sessionId = getSessionId(input);
8875
+ if (!sessionId) {
8876
+ throw new Error('sessionId is required');
8877
+ }
8878
+ const wait = AutomationWaitDownloadSchema.parse({ ...input, waitKind: 'download' });
8879
+ const waited = await waitForDownloadCondition(sessionId, wait, captureClient);
8880
+ return {
8881
+ ...createBaseResponse(sessionId),
8882
+ limitsApplied: {
8883
+ maxResults: 1,
8884
+ truncated: false,
8885
+ },
8886
+ ...waited,
8887
+ };
8888
+ },
8889
+ wait_for_popup: async (input) => {
8890
+ const sessionId = getSessionId(input);
8891
+ if (!sessionId) {
8892
+ throw new Error('sessionId is required');
8893
+ }
8894
+ const wait = AutomationWaitPopupSchema.parse({ ...input, waitKind: 'popup' });
8895
+ const waited = await waitForPopupCondition(sessionId, wait, captureClient);
8896
+ return {
8897
+ ...createBaseResponse(sessionId),
8898
+ limitsApplied: {
8899
+ maxResults: 1,
8900
+ truncated: false,
8901
+ },
8902
+ ...waited,
8903
+ };
8904
+ },
8905
+ wait_for_network_quiet: async (input) => {
8906
+ const sessionId = getSessionId(input);
8907
+ if (!sessionId) {
8908
+ throw new Error('sessionId is required');
8909
+ }
8910
+ if (!getDb) {
8911
+ throw new Error('wait_for_network_quiet requires database access');
8912
+ }
8913
+ const wait = AutomationWaitNetworkQuietSchema.parse({ ...input, waitKind: 'network_quiet' });
8914
+ const waited = await waitForNetworkQuietCondition(sessionId, wait, getDb());
8915
+ return {
8916
+ ...createBaseResponse(sessionId),
8917
+ limitsApplied: {
8918
+ maxResults: 10,
8919
+ truncated: false,
8920
+ },
8921
+ ...waited,
8922
+ };
8923
+ },
6506
8924
  run_ui_steps: async (input) => {
6507
8925
  const request = RunUIStepsSchema.parse(input);
6508
8926
  const workflowTraceId = createUIWorkflowTraceId();
@@ -6530,7 +8948,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6530
8948
  const previousCapture = lastPageCapture;
6531
8949
  try {
6532
8950
  if (step.kind === 'action') {
6533
- const resolvedTarget = await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
8951
+ const resolvedTarget = step.target?.locator
8952
+ ? {
8953
+ target: step.target,
8954
+ resolution: {
8955
+ strategy: 'native_locator_pending',
8956
+ matcher: summarizeWorkflowTargetMatcher(step.target),
8957
+ },
8958
+ pageCapture: undefined,
8959
+ }
8960
+ : await resolveWorkflowActionTarget(request.sessionId, step.target, workflowCapturePageState, request.mode === 'fast' ? lastPageCapture : undefined);
6534
8961
  const liveRequest = LiveUIActionRequestSchema.parse({
6535
8962
  action: step.action,
6536
8963
  target: resolvedTarget.target,
@@ -6541,6 +8968,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6541
8968
  const payload = ensureCaptureSuccess(capture, request.sessionId);
6542
8969
  const actionResult = payload;
6543
8970
  const failed = actionResult.status === 'failed' || actionResult.status === 'rejected';
8971
+ const actionResultPayload = typeof actionResult.result === 'object' && actionResult.result !== null
8972
+ ? actionResult.result
8973
+ : undefined;
8974
+ const nativeLocatorResolution = typeof actionResultPayload?.locatorResolution === 'object' && actionResultPayload.locatorResolution !== null
8975
+ ? actionResultPayload.locatorResolution
8976
+ : undefined;
6544
8977
  let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
6545
8978
  if (!failed && request.mode === 'fast') {
6546
8979
  await sleep(75);
@@ -6555,7 +8988,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6555
8988
  action: step.action,
6556
8989
  traceId: actionResult.traceId,
6557
8990
  target: {
6558
- resolution: resolvedTarget.resolution,
8991
+ resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
6559
8992
  actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
6560
8993
  ? actionResult.target
6561
8994
  : undefined,
@@ -6568,6 +9001,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6568
9001
  : undefined,
6569
9002
  pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
6570
9003
  };
9004
+ if (failed && getDb && finalStepResult.traceId) {
9005
+ mergeAutomationDiagnosticsEvidence(getDb(), {
9006
+ sessionId: request.sessionId,
9007
+ traceId: finalStepResult.traceId,
9008
+ failureEvidence: finalStepResult.failureEvidence,
9009
+ cdpFailure: actionResult.failureReason,
9010
+ });
9011
+ }
6571
9012
  }
6572
9013
  else if (step.kind === 'waitFor') {
6573
9014
  const waitInput = {
@@ -6595,6 +9036,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6595
9036
  pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
6596
9037
  };
6597
9038
  }
9039
+ else if (step.kind === 'wait') {
9040
+ const waitSpec = AutomationWaitSpecSchema.parse({
9041
+ ...step.wait,
9042
+ timeoutMs: step.wait.timeoutMs ?? request.defaultTimeoutMs,
9043
+ pollIntervalMs: step.wait.pollIntervalMs ?? request.defaultPollIntervalMs,
9044
+ });
9045
+ const waited = await runAutomationWait({
9046
+ sessionId: request.sessionId,
9047
+ wait: waitSpec,
9048
+ capturePageState: workflowCapturePageState,
9049
+ captureClient,
9050
+ getDb,
9051
+ });
9052
+ if (waited.waitKind === 'url' || waited.waitKind === 'navigation' || waited.waitKind === 'load_state') {
9053
+ lastPageCapture = await workflowCapturePageState(request.sessionId, {
9054
+ includeButtons: true,
9055
+ includeLinks: true,
9056
+ includeInputs: true,
9057
+ includeModals: true,
9058
+ maxItems: request.mode === 'fast' ? 12 : 20,
9059
+ maxTextLength: request.mode === 'fast' ? 60 : 80,
9060
+ }).catch(() => lastPageCapture);
9061
+ }
9062
+ finalStepResult = {
9063
+ id: stepId,
9064
+ kind: step.kind,
9065
+ status: waited.matched ? 'succeeded' : 'failed',
9066
+ durationMs: Math.max(0, Date.now() - startedAt),
9067
+ wait: {
9068
+ ...waitSpec,
9069
+ waitKind: waited.waitKind,
9070
+ matched: waited.matched,
9071
+ timeoutMs: waited.timeoutMs,
9072
+ pollIntervalMs: waited.pollIntervalMs,
9073
+ },
9074
+ waitedMs: waited.waitedMs,
9075
+ attempts: waited.attempts,
9076
+ error: waited.error,
9077
+ pageChangeSummary: createPageChangeSummary(previousCapture, lastPageCapture),
9078
+ target: waited.evidence,
9079
+ };
9080
+ }
6598
9081
  else {
6599
9082
  const capture = request.mode === 'fast' && lastPageCapture
6600
9083
  ? lastPageCapture
@@ -6629,7 +9112,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6629
9112
  target: step.kind === 'action' && workflowError
6630
9113
  ? workflowError.details
6631
9114
  : undefined,
6632
- matcher: step.kind === 'action' ? undefined : step.matcher,
9115
+ matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
9116
+ wait: step.kind === 'wait' ? step.wait : undefined,
6633
9117
  error: normalizeWorkflowError(error),
6634
9118
  };
6635
9119
  }
@@ -6652,6 +9136,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6652
9136
  if (evidence) {
6653
9137
  failureCaptureCount += 1;
6654
9138
  finalStepResult.failureEvidence = evidence;
9139
+ if (getDb && finalStepResult.traceId) {
9140
+ mergeAutomationDiagnosticsEvidence(getDb(), {
9141
+ sessionId: request.sessionId,
9142
+ traceId: finalStepResult.traceId,
9143
+ failureEvidence: evidence,
9144
+ cdpFailure: finalStepResult.error,
9145
+ });
9146
+ }
6655
9147
  }
6656
9148
  }
6657
9149
  stepResults.push(finalStepResult);
@@ -6671,7 +9163,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6671
9163
  status: 'skipped',
6672
9164
  durationMs: 0,
6673
9165
  action: step.kind === 'action' ? step.action : undefined,
6674
- matcher: step.kind === 'action' ? undefined : step.matcher,
9166
+ matcher: step.kind === 'assert' || step.kind === 'waitFor' ? step.matcher : undefined,
9167
+ wait: step.kind === 'wait' ? step.wait : undefined,
6675
9168
  pageChangeSummary: undefined,
6676
9169
  error: {
6677
9170
  code: 'workflow_stopped_early',
@@ -6871,7 +9364,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6871
9364
  const actionInput = { ...input };
6872
9365
  delete actionInput.sessionId;
6873
9366
  delete actionInput.captureOnFailure;
6874
- const request = LiveUIActionRequestSchema.parse(actionInput);
9367
+ let request = LiveUIActionRequestSchema.parse(actionInput);
9368
+ let targetResolution;
9369
+ try {
9370
+ if (request.target?.locator) {
9371
+ targetResolution = {
9372
+ strategy: 'native_locator_pending',
9373
+ matcher: summarizeWorkflowTargetMatcher(request.target),
9374
+ };
9375
+ }
9376
+ else if (hasSemanticActionTargetMatcher(request.target)) {
9377
+ const resolvedTarget = await resolveWorkflowActionTarget(sessionId, request.target, capturePageState);
9378
+ targetResolution = resolvedTarget.resolution;
9379
+ request = LiveUIActionRequestSchema.parse({
9380
+ ...request,
9381
+ target: resolvedTarget.target,
9382
+ });
9383
+ }
9384
+ }
9385
+ catch (error) {
9386
+ if (error instanceof WorkflowTargetResolutionError) {
9387
+ const traceId = request.traceId ?? `uiaction-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
9388
+ return {
9389
+ ...createBaseResponse(sessionId),
9390
+ limitsApplied: {
9391
+ maxResults: 1,
9392
+ truncated: false,
9393
+ },
9394
+ action: request.action,
9395
+ status: 'rejected',
9396
+ traceId,
9397
+ startedAt: Date.now(),
9398
+ finishedAt: Date.now(),
9399
+ durationMs: 0,
9400
+ target: {
9401
+ matched: false,
9402
+ },
9403
+ tabContext: {
9404
+ frameId: 0,
9405
+ },
9406
+ failureDetails: {
9407
+ code: error.code,
9408
+ message: error.message,
9409
+ },
9410
+ targetResolution: {
9411
+ ...error.details,
9412
+ strategy: 'semantic_failed',
9413
+ },
9414
+ supportedScopes: {
9415
+ executionScope: 'top-document-v1',
9416
+ topDocumentOnly: false,
9417
+ opensNewBrowserSession: false,
9418
+ },
9419
+ };
9420
+ }
9421
+ throw error;
9422
+ }
6875
9423
  const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
6876
9424
  const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
6877
9425
  const payload = ensureCaptureSuccess(capture, sessionId);
@@ -6896,6 +9444,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6896
9444
  const target = typeof actionResult.target === 'object' && actionResult.target !== null
6897
9445
  ? actionResult.target
6898
9446
  : {};
9447
+ const actionResultRecord = actionResult;
9448
+ const nativeResult = typeof actionResultRecord.result === 'object' && actionResultRecord.result !== null
9449
+ ? actionResultRecord.result
9450
+ : undefined;
9451
+ const nativeLocatorResolution = typeof nativeResult?.locatorResolution === 'object' && nativeResult.locatorResolution !== null
9452
+ ? nativeResult.locatorResolution
9453
+ : undefined;
9454
+ if (nativeLocatorResolution) {
9455
+ targetResolution = nativeLocatorResolution;
9456
+ }
9457
+ if (failed && getDb && actionResult.traceId) {
9458
+ mergeAutomationDiagnosticsEvidence(getDb(), {
9459
+ sessionId,
9460
+ traceId: actionResult.traceId,
9461
+ failureEvidence,
9462
+ cdpFailure: actionResult.failureReason,
9463
+ });
9464
+ }
6899
9465
  return {
6900
9466
  ...createBaseResponse(sessionId),
6901
9467
  limitsApplied: {
@@ -6914,6 +9480,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6914
9480
  : undefined,
6915
9481
  actionResult,
6916
9482
  target,
9483
+ targetResolution,
6917
9484
  tabContext: {
6918
9485
  tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
6919
9486
  frameId: typeof target.frameId === 'number' ? target.frameId : 0,
@@ -6924,7 +9491,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6924
9491
  postActionState,
6925
9492
  supportedScopes: {
6926
9493
  executionScope: actionResult.executionScope,
6927
- topDocumentOnly: true,
9494
+ topDocumentOnly: false,
6928
9495
  opensNewBrowserSession: false,
6929
9496
  },
6930
9497
  };
@@ -6976,13 +9543,37 @@ export function createToolRegistry(overrides = {}) {
6976
9543
  };
6977
9544
  });
6978
9545
  }
6979
- export async function routeToolCall(tools, toolName, input) {
9546
+ export async function routeToolCall(tools, toolName, input, options = {}) {
6980
9547
  const tool = tools.find((candidate) => candidate.name === toolName);
6981
9548
  if (!tool) {
6982
9549
  throw new Error(`Unknown tool: ${toolName}`);
6983
9550
  }
6984
- const response = await tool.handler(isRecord(input) ? input : {});
6985
- return attachResponseBytes(response);
9551
+ const normalizedInput = isRecord(input) ? input : {};
9552
+ const guardCall = options.loopGuard?.prepareCall(toolName, normalizedInput);
9553
+ const beforeCall = guardCall ? await options.loopGuard?.beforeCall(guardCall) : undefined;
9554
+ if (beforeCall?.blocked) {
9555
+ return attachResponseBytes(beforeCall.response);
9556
+ }
9557
+ const startedAt = Date.now();
9558
+ try {
9559
+ const response = await tool.handler(normalizedInput);
9560
+ const guarded = guardCall
9561
+ ? await options.loopGuard?.afterCall(guardCall, {
9562
+ response,
9563
+ durationMs: Date.now() - startedAt,
9564
+ })
9565
+ : undefined;
9566
+ return attachResponseBytes((guarded?.response ?? response));
9567
+ }
9568
+ catch (error) {
9569
+ if (guardCall) {
9570
+ await options.loopGuard?.afterCall(guardCall, {
9571
+ error,
9572
+ durationMs: Date.now() - startedAt,
9573
+ });
9574
+ }
9575
+ throw error;
9576
+ }
6986
9577
  }
6987
9578
  export function createMCPServer(overrides = {}, options = {}) {
6988
9579
  const logger = options.logger ?? createDefaultMcpLogger();
@@ -6994,6 +9585,17 @@ export function createMCPServer(overrides = {}, options = {}) {
6994
9585
  ...v2Handlers,
6995
9586
  ...overrides,
6996
9587
  });
9588
+ const loopGuard = options.loopGuard === false
9589
+ ? undefined
9590
+ : options.loopGuard ?? createToolLoopGuard({
9591
+ getDb: () => getConnection().db,
9592
+ onEvent: (event) => {
9593
+ logger.info({
9594
+ component: 'mcp',
9595
+ ...event,
9596
+ }, `[MCPServer][MCP] ${event.event}`);
9597
+ },
9598
+ });
6997
9599
  const server = new Server({
6998
9600
  name: 'browser-debug-mcp-bridge',
6999
9601
  version: '1.0.0',
@@ -7017,7 +9619,7 @@ export function createMCPServer(overrides = {}, options = {}) {
7017
9619
  const startedAt = Date.now();
7018
9620
  logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
7019
9621
  try {
7020
- const response = await routeToolCall(tools, toolName, request.params.arguments);
9622
+ const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
7021
9623
  logger.info({
7022
9624
  component: 'mcp',
7023
9625
  event: 'tool_call_completed',