browser-debug-mcp-bridge 1.11.0 → 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.
Files changed (26) hide show
  1. package/README.md +3 -1
  2. package/apps/mcp-server/dist/db/automation-repository.js +9 -4
  3. package/apps/mcp-server/dist/db/automation-repository.js.map +1 -1
  4. package/apps/mcp-server/dist/db/migrations.js +79 -0
  5. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  6. package/apps/mcp-server/dist/db/schema.js +60 -1
  7. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  8. package/apps/mcp-server/dist/mcp/server.js +3455 -357
  9. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  10. package/apps/mcp-server/dist/mcp/target-resolution.js +390 -0
  11. package/apps/mcp-server/dist/mcp/target-resolution.js.map +1 -0
  12. package/apps/mcp-server/dist/mcp/tool-loop-guard.js +655 -0
  13. package/apps/mcp-server/dist/mcp/tool-loop-guard.js.map +1 -0
  14. package/apps/mcp-server/dist/override-audit.js +3 -3
  15. package/apps/mcp-server/dist/override-audit.js.map +1 -1
  16. package/apps/mcp-server/dist/override-capabilities.js +22 -1
  17. package/apps/mcp-server/dist/override-capabilities.js.map +1 -1
  18. package/apps/mcp-server/dist/override-poc.js +4 -4
  19. package/apps/mcp-server/dist/override-poc.js.map +1 -1
  20. package/apps/mcp-server/dist/override-profile-generator.js +3 -9
  21. package/apps/mcp-server/dist/override-profile-generator.js.map +1 -1
  22. package/apps/mcp-server/dist/override-response-planner.js +6 -4
  23. package/apps/mcp-server/dist/override-response-planner.js.map +1 -1
  24. package/apps/mcp-server/dist/websocket/messages.js +5 -0
  25. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  26. package/package.json +8 -3
@@ -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,143 +975,356 @@ 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
- requestMethod: { type: 'string' },
653
- requestHeaders: { type: 'object' },
654
1095
  timeoutMs: { type: 'number' },
655
- maxBodyBytes: { type: 'number' },
656
- includeBody: { type: 'boolean' },
1096
+ pollIntervalMs: { type: 'number' },
657
1097
  },
658
1098
  },
659
- list_observed_override_assets: {
1099
+ wait_for_download: {
660
1100
  type: 'object',
661
1101
  required: ['sessionId'],
662
1102
  properties: {
663
1103
  sessionId: { type: 'string' },
664
- limit: { type: 'number' },
665
- 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' },
666
1113
  },
667
1114
  },
668
- map_next_override_assets: {
1115
+ wait_for_popup: {
669
1116
  type: 'object',
670
- required: ['projectRoot'],
1117
+ required: ['sessionId'],
671
1118
  properties: {
672
1119
  sessionId: { type: 'string' },
673
- tabId: { type: 'number' },
674
- projectRoot: { type: 'string' },
675
- nextDir: { type: 'string' },
676
- route: { type: 'string' },
677
- sourcePaths: { type: 'array', items: { type: 'string' } },
678
- observedAssets: { type: 'array', items: { type: 'object' } },
679
- maxResults: { type: 'number' },
680
- fetchProductionAssets: { type: 'boolean' },
681
- productionFetchTimeoutMs: { type: 'number' },
682
- maxProductionAssetBytes: { type: 'number' },
683
- maxDriftCandidates: { type: 'number' },
684
- 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' },
685
1126
  },
686
1127
  },
687
- plan_override_response_patch: {
1128
+ wait_for_request: {
688
1129
  type: 'object',
1130
+ required: ['sessionId'],
689
1131
  properties: {
690
1132
  sessionId: { type: 'string' },
691
- tabId: { type: 'number' },
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: {
1324
+ type: 'object',
1325
+ properties: {
1326
+ sessionId: { type: 'string' },
1327
+ tabId: { type: 'number' },
692
1328
  targetUrl: { type: 'string' },
693
1329
  targetAssetUrl: { type: 'string' },
694
1330
  captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
@@ -861,7 +1497,7 @@ const TOOL_SCHEMAS = {
861
1497
  properties: {
862
1498
  sessionId: { type: 'string' },
863
1499
  status: { type: 'string', enum: ['requested', 'started', 'succeeded', 'failed', 'rejected', 'stopped'] },
864
- 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'] },
865
1501
  traceId: { type: 'string' },
866
1502
  limit: { type: 'number' },
867
1503
  offset: { type: 'number' },
@@ -884,16 +1520,53 @@ const TOOL_SCHEMAS = {
884
1520
  required: ['sessionId', 'action'],
885
1521
  properties: {
886
1522
  sessionId: { type: 'string' },
887
- 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'] },
888
1524
  traceId: { type: 'string' },
889
1525
  target: {
890
1526
  type: 'object',
891
1527
  properties: {
892
1528
  selector: { type: 'string' },
893
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
+ },
894
1538
  tabId: { type: 'number' },
895
1539
  frameId: { type: 'number' },
896
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' },
897
1570
  },
898
1571
  },
899
1572
  input: { type: 'object' },
@@ -916,14 +1589,22 @@ const TOOL_SCHEMAS = {
916
1589
  type: 'object',
917
1590
  required: ['scope'],
918
1591
  properties: {
919
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1592
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
920
1593
  selector: { type: 'string' },
921
1594
  testId: { type: 'string' },
922
1595
  textContains: { type: 'string' },
923
1596
  labelContains: { type: 'string' },
924
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' },
925
1605
  urlContains: { type: 'string' },
926
1606
  language: { type: 'string' },
1607
+ visible: { type: 'boolean' },
927
1608
  disabled: { type: 'boolean' },
928
1609
  selected: { type: 'boolean' },
929
1610
  pressed: { type: 'boolean' },
@@ -960,7 +1641,7 @@ const TOOL_SCHEMAS = {
960
1641
  properties: {
961
1642
  id: { type: 'string' },
962
1643
  note: { type: 'string' },
963
- kind: { type: 'string', enum: ['action', 'waitFor', 'assert'] },
1644
+ kind: { type: 'string', enum: ['action', 'waitFor', 'assert', 'wait'] },
964
1645
  action: { type: 'string' },
965
1646
  traceId: { type: 'string' },
966
1647
  target: {
@@ -968,16 +1649,37 @@ const TOOL_SCHEMAS = {
968
1649
  properties: {
969
1650
  selector: { type: 'string' },
970
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
+ },
971
1660
  tabId: { type: 'number' },
972
1661
  frameId: { type: 'number' },
973
1662
  url: { type: 'string' },
1663
+ locator: ACTION_LOCATOR_TOOL_SCHEMA,
1664
+ frameUrlContains: { type: 'string' },
1665
+ frameTitleContains: { type: 'string' },
974
1666
  testId: { type: 'string' },
975
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused'] },
1667
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused'] },
976
1668
  textContains: { type: 'string' },
977
1669
  labelContains: { type: 'string' },
978
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' },
979
1680
  tagName: { type: 'string' },
980
1681
  type: { type: 'string' },
1682
+ visible: { type: 'boolean' },
981
1683
  disabled: { type: 'boolean' },
982
1684
  selected: { type: 'boolean' },
983
1685
  pressed: { type: 'boolean' },
@@ -1011,14 +1713,22 @@ const TOOL_SCHEMAS = {
1011
1713
  matcher: {
1012
1714
  type: 'object',
1013
1715
  properties: {
1014
- scope: { type: 'string', enum: ['buttons', 'inputs', 'modals', 'focused', 'page'] },
1716
+ scope: { type: 'string', enum: ['buttons', 'links', 'inputs', 'modals', 'focused', 'page'] },
1015
1717
  selector: { type: 'string' },
1016
1718
  testId: { type: 'string' },
1017
1719
  textContains: { type: 'string' },
1018
1720
  labelContains: { type: 'string' },
1019
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' },
1020
1729
  urlContains: { type: 'string' },
1021
1730
  language: { type: 'string' },
1731
+ visible: { type: 'boolean' },
1022
1732
  disabled: { type: 'boolean' },
1023
1733
  selected: { type: 'boolean' },
1024
1734
  pressed: { type: 'boolean' },
@@ -1035,6 +1745,7 @@ const TOOL_SCHEMAS = {
1035
1745
  pollIntervalMs: { type: 'number' },
1036
1746
  },
1037
1747
  },
1748
+ wait: AUTOMATION_WAIT_TOOL_SCHEMA,
1038
1749
  },
1039
1750
  },
1040
1751
  },
@@ -1061,11 +1772,25 @@ const TOOL_DESCRIPTIONS = {
1061
1772
  get_computed_styles: 'Read computed CSS styles for an element',
1062
1773
  get_layout_metrics: 'Read viewport and element layout metrics',
1063
1774
  get_page_state: 'Read a compact structured page model for forms, buttons, modals, and viewport state',
1064
- 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',
1065
1776
  get_live_session_health: 'Read live transport health and session binding details for one session',
1066
1777
  set_viewport: 'Resize the live browser window for a session and return the resulting viewport metrics',
1067
1778
  assert_page_state: 'Assert compact page-state conditions without pulling raw DOM payloads',
1068
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',
1069
1794
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
1070
1795
  get_live_console_logs: 'Read in-memory live console logs for a connected session',
1071
1796
  list_override_profiles: 'List configured browser override profiles',
@@ -1112,7 +1837,10 @@ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
1112
1837
  const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
1113
1838
  const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
1114
1839
  const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
1840
+ const DEFAULT_AUTOMATION_WAIT_LOOKBACK_MS = 5_000;
1115
1841
  const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
1842
+ const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
1843
+ const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
1116
1844
  const STALE_LIVE_CONNECTION_GRACE_WINDOW_MS = 30 * 60 * 1000;
1117
1845
  const NOISE_SESSION_HOST_PATTERNS = [
1118
1846
  /(^|\.)adtrafficquality\.google$/i,
@@ -1392,7 +2120,7 @@ function buildOverrideProfileRecords() {
1392
2120
  active: profile.profileId === summary.activeProfileId,
1393
2121
  configEnabled: summary.configEnabled,
1394
2122
  enabled: profile.enabled,
1395
- effectiveEnabled: summary.configEnabled && profile.enabled && profile.enabledRuleCount > 0,
2123
+ effectiveEnabled: profile.enabled && profile.enabledRuleCount > 0,
1396
2124
  autoReload: profile.autoReload,
1397
2125
  configPath: summary.configPath,
1398
2126
  fileExists: profile.fileExists,
@@ -1411,6 +2139,72 @@ function resolveOverrideProfileRecord(value) {
1411
2139
  }
1412
2140
  return profile;
1413
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
+ }
1414
2208
  const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
1415
2209
  function sha256Text(value) {
1416
2210
  return createHash('sha256').update(value, 'utf8').digest('hex');
@@ -1422,6 +2216,29 @@ function isRecordWithRscFlightMetadata(value) {
1422
2216
  && value.source !== undefined
1423
2217
  && value.patchKind !== undefined;
1424
2218
  }
2219
+ function normalizeRuleStringHeaders(value) {
2220
+ if (!isRecord(value)) {
2221
+ return undefined;
2222
+ }
2223
+ const headers = {};
2224
+ for (const [name, rawValue] of Object.entries(value)) {
2225
+ if (typeof rawValue !== 'string') {
2226
+ continue;
2227
+ }
2228
+ const normalizedName = name.trim().toLowerCase();
2229
+ const normalizedValue = rawValue.trim();
2230
+ if (normalizedName.length > 0 && normalizedValue.length > 0) {
2231
+ headers[normalizedName] = normalizedValue;
2232
+ }
2233
+ }
2234
+ return Object.keys(headers).length > 0 ? headers : undefined;
2235
+ }
2236
+ function getRscFlightRuleRequestHeaders(rule) {
2237
+ return isRecord(rule.rscFlight) ? normalizeRuleStringHeaders(rule.rscFlight.requestHeaders) : undefined;
2238
+ }
2239
+ function getOverrideRuleRequestHeaders(rule) {
2240
+ return normalizeRuleStringHeaders(rule.requestHeaders) ?? getRscFlightRuleRequestHeaders(rule);
2241
+ }
1425
2242
  function buildRscFlightRuleIssues(rule) {
1426
2243
  const ruleId = String(rule.ruleId ?? 'unknown');
1427
2244
  const issues = [];
@@ -1465,17 +2282,20 @@ function buildRscFlightRuleIssues(rule) {
1465
2282
  }
1466
2283
  }
1467
2284
  }
1468
- if (rule.requestMethod !== 'GET') {
2285
+ const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
2286
+ const requestHeaders = getRscFlightRuleRequestHeaders(rule);
2287
+ const isCapturedPostRscFlight = requestMethod === 'POST' && requestHeaders?.rsc === '1';
2288
+ if (requestMethod !== 'GET' && !isCapturedPostRscFlight) {
1469
2289
  issues.push({
1470
2290
  code: 'RSC_FLIGHT_METHOD_UNSUPPORTED',
1471
2291
  severity: 'error',
1472
- message: `Rule ${ruleId} RSC flight overrides only support GET requests.`,
2292
+ message: `Rule ${ruleId} RSC flight overrides only support GET requests or captured POST RSC response-stage patches.`,
1473
2293
  });
1474
2294
  }
1475
2295
  const targetAssetUrl = typeof rule.targetAssetUrl === 'string' ? rule.targetAssetUrl : '';
1476
2296
  try {
1477
2297
  const parsed = new URL(targetAssetUrl);
1478
- if (!parsed.searchParams.has('_rsc')) {
2298
+ if (requestMethod === 'GET' && !parsed.searchParams.has('_rsc')) {
1479
2299
  issues.push({
1480
2300
  code: 'RSC_FLIGHT_TARGET_INVALID',
1481
2301
  severity: 'error',
@@ -1553,13 +2373,6 @@ function buildOverrideProfileIssues(profile) {
1553
2373
  const rules = Array.isArray(profile.rules)
1554
2374
  ? profile.rules.filter((rule) => isRecord(rule))
1555
2375
  : [];
1556
- if (profile.configEnabled !== true) {
1557
- issues.push({
1558
- code: 'CONFIG_DISABLED',
1559
- severity: 'warning',
1560
- message: 'The override config is disabled and cannot replace requests until enabled.',
1561
- });
1562
- }
1563
2376
  if (profile.enabled !== true) {
1564
2377
  issues.push({
1565
2378
  code: 'PROFILE_DISABLED',
@@ -1595,7 +2408,7 @@ function buildOverrideProfileIssues(profile) {
1595
2408
  issues.push(...classifyOverrideResponseRequestCapability({
1596
2409
  ruleId: rule.ruleId,
1597
2410
  requestMethod: rule.requestMethod,
1598
- requestHeaders: rule.requestHeaders,
2411
+ requestHeaders: getOverrideRuleRequestHeaders(rule),
1599
2412
  ruleType: rule.ruleType,
1600
2413
  }).issues.map((issue) => ({ ...issue })));
1601
2414
  if (rule.ruleType === 'rsc-flight') {
@@ -1648,12 +2461,6 @@ function buildOverrideProfileNextActions(profile, issues) {
1648
2461
  message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
1649
2462
  }];
1650
2463
  }
1651
- if (profile.configEnabled !== true) {
1652
- return [{
1653
- code: 'ENABLE_CONFIG',
1654
- message: 'Set the root override config enabled=true after reviewing the profile.',
1655
- }];
1656
- }
1657
2464
  if (profile.enabled !== true) {
1658
2465
  return [{
1659
2466
  code: 'ENABLE_PROFILE',
@@ -1676,8 +2483,14 @@ function hasEnabledExperimentalRscFlightRule(profile) {
1676
2483
  });
1677
2484
  }
1678
2485
  function canBypassPreflightForExperimentalRsc(profile, blockingCodes) {
2486
+ const allowedExperimentalBlockers = new Set([
2487
+ 'UNSUPPORTED_RSC_FLIGHT_RULE',
2488
+ 'NO_OBSERVED_ASSETS',
2489
+ 'TARGET_ASSET_NOT_OBSERVED',
2490
+ ]);
1679
2491
  return blockingCodes.length > 0
1680
- && blockingCodes.every((code) => code === 'UNSUPPORTED_RSC_FLIGHT_RULE')
2492
+ && blockingCodes.includes('UNSUPPORTED_RSC_FLIGHT_RULE')
2493
+ && blockingCodes.every((code) => allowedExperimentalBlockers.has(code))
1681
2494
  && hasEnabledExperimentalRscFlightRule(profile);
1682
2495
  }
1683
2496
  const OVERRIDE_VARIANT_HEADER_ALLOWLIST = new Set([
@@ -1797,6 +2610,36 @@ function pushOverridePreflightIssue(issues, issue) {
1797
2610
  }
1798
2611
  issues.push(issue);
1799
2612
  }
2613
+ function getPreflightIssues(preflight) {
2614
+ return Array.isArray(preflight?.issues)
2615
+ ? preflight.issues.filter((issue) => isRecord(issue))
2616
+ : [];
2617
+ }
2618
+ function getBlockingPreflightCodes(preflight) {
2619
+ return getPreflightIssues(preflight)
2620
+ .filter((issue) => issue.severity === 'error')
2621
+ .map((issue) => String(issue.code ?? 'UNKNOWN'));
2622
+ }
2623
+ function hasPreflightIssue(preflight, codes) {
2624
+ const expected = new Set(codes);
2625
+ return getPreflightIssues(preflight).some((issue) => expected.has(String(issue.code ?? '')));
2626
+ }
2627
+ function shouldRefreshObservedAssetsForEnable(preflight) {
2628
+ const assetReadinessCodes = new Set([
2629
+ 'NO_OBSERVED_ASSETS',
2630
+ 'TARGET_ASSET_NOT_OBSERVED',
2631
+ 'SESSION_SCOPE_DRIFT',
2632
+ ]);
2633
+ const blockingCodes = getBlockingPreflightCodes(preflight);
2634
+ if (blockingCodes.length === 0 || !blockingCodes.every((code) => assetReadinessCodes.has(code))) {
2635
+ return false;
2636
+ }
2637
+ return hasPreflightIssue(preflight, [
2638
+ 'NO_OBSERVED_ASSETS',
2639
+ 'TARGET_ASSET_NOT_OBSERVED',
2640
+ 'SESSION_SCOPE_DRIFT',
2641
+ ]);
2642
+ }
1800
2643
  function buildOverridePreflight(options) {
1801
2644
  const session = options.db
1802
2645
  .prepare(`
@@ -1835,7 +2678,20 @@ function buildOverridePreflight(options) {
1835
2678
  .filter((context) => context !== null)
1836
2679
  .map((context) => [String(context.variantKey ?? JSON.stringify(context)), context])).values()];
1837
2680
  const sessionState = options.getSessionConnectionState?.(options.sessionId);
2681
+ const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
1838
2682
  const diagnosis = session ? diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId) : null;
2683
+ const observedAssetTabs = [...new Set(observedAssets
2684
+ .map((asset) => asset.tabId)
2685
+ .filter((tabId) => typeof tabId === 'number' && Number.isFinite(tabId)))].sort((a, b) => a - b);
2686
+ const observedAssetPageUrls = [...new Set(observedAssets
2687
+ .map((asset) => asset.pageUrl)
2688
+ .filter((pageUrl) => typeof pageUrl === 'string' && pageUrl.trim().length > 0))].slice(0, 5);
2689
+ const sessionTabId = typeof session?.tab_id === 'number' ? session.tab_id : undefined;
2690
+ const observedAssetsWithKnownTabs = observedAssets.filter((asset) => typeof asset.tabId === 'number');
2691
+ const topLevelScopeLikely = sessionTabId === undefined
2692
+ || observedAssets.length === 0
2693
+ || observedAssetsWithKnownTabs.length === 0
2694
+ || observedAssetsWithKnownTabs.some((asset) => asset.tabId === sessionTabId);
1839
2695
  for (const issue of buildOverrideProfileIssues(profile)) {
1840
2696
  pushOverridePreflightIssue(issues, { ...issue, source: 'profile' });
1841
2697
  }
@@ -1865,55 +2721,117 @@ function buildOverridePreflight(options) {
1865
2721
  message: `Session ${options.sessionId} has ended and cannot enable overrides.`,
1866
2722
  });
1867
2723
  }
1868
- if (sessionState && sessionState.connected !== true) {
2724
+ if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
1869
2725
  pushOverridePreflightIssue(issues, {
1870
2726
  code: LIVE_SESSION_DISCONNECTED_CODE,
1871
2727
  severity: 'error',
1872
2728
  source: 'connection',
1873
- message: `Session ${options.sessionId} is not currently connected to the live extension bridge.`,
2729
+ message: sessionState
2730
+ ? `Session ${options.sessionId} is not currently connected to the live extension bridge. Last disconnect reason: ${sessionState.disconnectReason ?? 'unknown'}.`
2731
+ : `Session ${options.sessionId} has no current live extension connection state.`,
2732
+ disconnectedAt: sessionState?.disconnectedAt,
2733
+ disconnectReason: sessionState?.disconnectReason,
1874
2734
  });
1875
2735
  }
1876
2736
  }
1877
2737
  const enabledRules = Array.isArray(profile.rules)
1878
2738
  ? profile.rules.filter((rule) => isRecord(rule) && rule.enabled === true)
1879
2739
  : [];
2740
+ const enabledRuleAssetReadiness = enabledRules
2741
+ .map((rule) => {
2742
+ const targetAssetUrl = normalizeOptionalString(rule.targetAssetUrl);
2743
+ if (!targetAssetUrl) {
2744
+ return null;
2745
+ }
2746
+ const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
2747
+ const matchMode = String(rule.matchMode ?? 'exact');
2748
+ const matchingAssets = observedAssets.filter((asset) => {
2749
+ const methodMatches = normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
2750
+ if (!methodMatches) {
2751
+ return false;
2752
+ }
2753
+ return matchMode === 'prefix'
2754
+ ? asset.url.startsWith(targetAssetUrl)
2755
+ : asset.url === targetAssetUrl;
2756
+ });
2757
+ return {
2758
+ ruleId: String(rule.ruleId ?? 'unknown'),
2759
+ targetAssetUrl,
2760
+ requestMethod,
2761
+ matchMode,
2762
+ captureProven: rule.ruleType === 'rsc-flight' && isRecordWithRscFlightMetadata(rule.rscFlight),
2763
+ matchingAssets,
2764
+ };
2765
+ })
2766
+ .filter((readiness) => readiness !== null);
2767
+ const matchedTargetAssetCount = enabledRuleAssetReadiness.filter((readiness) => readiness.matchingAssets.length > 0).length;
2768
+ const capturedTargetAssetCount = enabledRuleAssetReadiness
2769
+ .filter((readiness) => readiness.matchingAssets.length === 0 && readiness.captureProven)
2770
+ .length;
2771
+ const unobservedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount;
2772
+ const unsatisfiedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount - capturedTargetAssetCount;
2773
+ const targetAssetObserved = observedAssets.length > 0 && matchedTargetAssetCount > 0;
2774
+ const targetAssetReadinessSatisfied = observedAssets.length > 0
2775
+ && (enabledRuleAssetReadiness.length === 0 || matchedTargetAssetCount > 0 || capturedTargetAssetCount > 0);
1880
2776
  const anyServiceWorkerControlled = observedAssets.some((asset) => asset.serviceWorkerControlled);
1881
2777
  const cspMetaTags = [...new Set(observedAssets.flatMap((asset) => asset.cspMetaTags))];
1882
2778
  if (observedAssets.length === 0) {
1883
2779
  pushOverridePreflightIssue(issues, {
1884
2780
  code: 'NO_OBSERVED_ASSETS',
1885
- severity: 'warning',
2781
+ severity: 'error',
1886
2782
  source: 'observed-assets',
1887
- message: 'No observed production assets are stored for this session yet.',
2783
+ message: 'No observed production assets are stored for this session yet; the target route is not capture-ready for override enablement.',
1888
2784
  });
1889
2785
  }
1890
- for (const rule of enabledRules) {
1891
- const ruleId = String(rule.ruleId ?? 'unknown');
1892
- const targetAssetUrl = normalizeOptionalString(rule.targetAssetUrl);
1893
- if (!targetAssetUrl) {
1894
- continue;
1895
- }
1896
- const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
1897
- const matchingAssets = observedAssets.filter((asset) => {
1898
- return asset.url === targetAssetUrl
1899
- && normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
2786
+ else if (!topLevelScopeLikely) {
2787
+ pushOverridePreflightIssue(issues, {
2788
+ code: 'SESSION_SCOPE_DRIFT',
2789
+ severity: 'error',
2790
+ source: 'observed-assets',
2791
+ message: `Observed override assets were recorded only for tab(s) ${observedAssetTabs.join(', ')}, but the session top-level tab is ${sessionTabId}.`,
2792
+ observedAssetTabs,
2793
+ sessionTabId,
2794
+ observedPageUrls: observedAssetPageUrls,
1900
2795
  });
1901
- if (observedAssets.length > 0 && matchingAssets.length === 0) {
2796
+ }
2797
+ if (observedAssets.length > 0 && enabledRuleAssetReadiness.length > 0 && matchedTargetAssetCount === 0 && capturedTargetAssetCount === 0) {
2798
+ const sampleTargets = enabledRuleAssetReadiness.slice(0, 5).map((readiness) => ({
2799
+ ruleId: readiness.ruleId,
2800
+ requestMethod: readiness.requestMethod,
2801
+ matchMode: readiness.matchMode,
2802
+ targetAssetUrl: readiness.targetAssetUrl,
2803
+ }));
2804
+ pushOverridePreflightIssue(issues, {
2805
+ code: 'TARGET_ASSET_NOT_OBSERVED',
2806
+ severity: 'error',
2807
+ source: 'observed-assets',
2808
+ message: enabledRuleAssetReadiness.length === 1
2809
+ ? `Rule ${enabledRuleAssetReadiness[0].ruleId} target asset was not observed for ${enabledRuleAssetReadiness[0].requestMethod} ${enabledRuleAssetReadiness[0].targetAssetUrl}.`
2810
+ : `None of the ${enabledRuleAssetReadiness.length} enabled override targets were observed for this session.`,
2811
+ checkedTargetAssetCount: enabledRuleAssetReadiness.length,
2812
+ sampleTargets,
2813
+ });
2814
+ }
2815
+ for (const readiness of enabledRuleAssetReadiness) {
2816
+ if (readiness.matchingAssets.length === 0) {
2817
+ if (readiness.captureProven) {
2818
+ continue;
2819
+ }
1902
2820
  pushOverridePreflightIssue(issues, {
1903
- code: 'TARGET_ASSET_NOT_OBSERVED',
2821
+ code: 'TARGET_ASSET_NOT_OBSERVED_FOR_RULE',
1904
2822
  severity: 'warning',
1905
2823
  source: 'observed-assets',
1906
- message: `Rule ${ruleId} target asset was not observed for ${requestMethod} ${targetAssetUrl}.`,
2824
+ message: `Rule ${readiness.ruleId} target asset was not observed for ${readiness.requestMethod} ${readiness.targetAssetUrl}.`,
1907
2825
  });
1908
2826
  continue;
1909
2827
  }
1910
- for (const asset of matchingAssets) {
2828
+ for (const asset of readiness.matchingAssets) {
1911
2829
  if (typeof asset.integrity === 'string' && asset.integrity.length > 0) {
1912
2830
  pushOverridePreflightIssue(issues, {
1913
2831
  code: 'TARGET_ASSET_SRI_PRESENT',
1914
2832
  severity: 'error',
1915
2833
  source: 'observed-assets',
1916
- message: `Rule ${ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
2834
+ message: `Rule ${readiness.ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
1917
2835
  });
1918
2836
  }
1919
2837
  }
@@ -1936,23 +2854,29 @@ function buildOverridePreflight(options) {
1936
2854
  }
1937
2855
  const ready = !issues.some((issue) => issue.severity === 'error');
1938
2856
  const nextActions = !ready
1939
- ? issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')
1940
- ? [{
1941
- code: 'REPLAN_SERVER_ACTION_OVERRIDE',
1942
- message: 'Server actions stay unsupported in production override mode; move the override to a GET document/data/API response.',
1943
- }]
1944
- : issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')
1945
- ? [{
1946
- code: 'REPLAN_MUTATION_OVERRIDE',
1947
- message: 'Mutation responses are not replay-safe; use a GET document/data/API response path instead.',
1948
- }]
1949
- : issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')
1950
- ? [{ code: 'REPLAN_GET_ONLY_OVERRIDE', message: 'Remove or regenerate non-GET rules before enabling overrides.' }]
1951
- : issues.some((issue) => issue.code === 'TARGET_ASSET_SRI_PRESENT')
1952
- ? [{ code: 'CHOOSE_ANOTHER_OVERRIDE_PATH', message: 'Choose a document/data response path or remove SRI on the production asset before enabling overrides.' }]
1953
- : issues.some((issue) => issue.code === 'SESSION_NOT_FOUND' || issue.code === 'SESSION_PAUSED' || issue.code === 'SESSION_ENDED' || issue.code === LIVE_SESSION_DISCONNECTED_CODE)
1954
- ? [{ code: 'RECONNECT_SESSION', message: 'Reconnect or resume the target session before enabling overrides.' }]
1955
- : buildOverrideProfileNextActions(profile, issues)
2857
+ ? issues.some((issue) => issue.code === 'SESSION_NOT_FOUND' || issue.code === 'SESSION_PAUSED' || issue.code === 'SESSION_ENDED' || issue.code === LIVE_SESSION_DISCONNECTED_CODE)
2858
+ ? [{ code: 'RECONNECT_SESSION', message: 'Reconnect or resume the target session before enabling overrides.' }]
2859
+ : issues.some((issue) => issue.code === 'SESSION_SCOPE_DRIFT')
2860
+ ? [{ code: 'FOCUS_BOUND_TAB', message: 'Focus or reselect the bound top-level tab, then observe override assets again.' }]
2861
+ : issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')
2862
+ ? [{
2863
+ code: 'REPLAN_SERVER_ACTION_OVERRIDE',
2864
+ message: 'Server actions stay unsupported in production override mode; move the override to a GET document/data/API response.',
2865
+ }]
2866
+ : issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')
2867
+ ? [{
2868
+ code: 'REPLAN_MUTATION_OVERRIDE',
2869
+ message: 'Mutation responses are not replay-safe; use a GET document/data/API response path instead.',
2870
+ }]
2871
+ : issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')
2872
+ ? [{ code: 'REPLAN_GET_ONLY_OVERRIDE', message: 'Remove or regenerate non-GET rules before enabling overrides.' }]
2873
+ : issues.some((issue) => issue.code === 'TARGET_ASSET_SRI_PRESENT')
2874
+ ? [{ code: 'CHOOSE_ANOTHER_OVERRIDE_PATH', message: 'Choose a document/data response path or remove SRI on the production asset before enabling overrides.' }]
2875
+ : issues.some((issue) => issue.code === 'NO_OBSERVED_ASSETS')
2876
+ ? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Observe the bound target route before enabling overrides.' }]
2877
+ : issues.some((issue) => issue.code === 'TARGET_ASSET_NOT_OBSERVED')
2878
+ ? [{ code: 'OBSERVE_TARGET_ROUTE', message: 'Load the route that requests the configured target and observe assets again.' }]
2879
+ : buildOverrideProfileNextActions(profile, issues)
1956
2880
  : observedAssets.length === 0
1957
2881
  ? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Run observe_override_assets on the target route before enabling overrides in production workflows.' }]
1958
2882
  : [{ code: 'ENABLE_OVERRIDES', message: 'Preflight checks passed; the selected profile can be enabled on the live session.' }];
@@ -1976,8 +2900,22 @@ function buildOverridePreflight(options) {
1976
2900
  checks: {
1977
2901
  sessionFound: session !== undefined,
1978
2902
  connected: sessionState?.connected === true,
2903
+ captureReady: session !== undefined
2904
+ && getSessionStatus(session) === 'active'
2905
+ && (!hasLiveConnectionLookup || sessionState?.connected === true)
2906
+ && observedAssets.length > 0
2907
+ && topLevelScopeLikely
2908
+ && !issues.some((issue) => issue.severity === 'error'),
2909
+ topLevelScopeLikely,
2910
+ observedAssetTabs,
2911
+ observedAssetPageUrls,
1979
2912
  observedAssetCount: observedAssets.length,
1980
- targetAssetObserved: issues.every((issue) => issue.code !== 'TARGET_ASSET_NOT_OBSERVED'),
2913
+ targetAssetObserved,
2914
+ targetAssetReadinessSatisfied,
2915
+ matchedTargetAssetCount,
2916
+ capturedTargetAssetCount,
2917
+ unobservedTargetAssetCount,
2918
+ unsatisfiedTargetAssetCount,
1981
2919
  serviceWorkerControlled: anyServiceWorkerControlled,
1982
2920
  cspMetaTagCount: cspMetaTags.length,
1983
2921
  recentPlanCount: recentPlans.length,
@@ -1985,6 +2923,14 @@ function buildOverridePreflight(options) {
1985
2923
  },
1986
2924
  observedAssets: {
1987
2925
  count: observedAssets.length,
2926
+ tabIds: observedAssetTabs,
2927
+ pageUrls: observedAssetPageUrls,
2928
+ targetAssetObserved,
2929
+ targetAssetReadinessSatisfied,
2930
+ matchedTargetAssetCount,
2931
+ capturedTargetAssetCount,
2932
+ unobservedTargetAssetCount,
2933
+ unsatisfiedTargetAssetCount,
1988
2934
  serviceWorkerControlled: anyServiceWorkerControlled,
1989
2935
  cspMetaTags,
1990
2936
  },
@@ -2390,6 +3336,7 @@ function mapAutomationRunRecord(row) {
2390
3336
  : undefined,
2391
3337
  stopReason: row.stop_reason ?? undefined,
2392
3338
  target: parseJsonOrUndefined(row.target_summary_json),
3339
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2393
3340
  failure: parseJsonOrUndefined(row.failure_json),
2394
3341
  redaction: parseJsonOrUndefined(row.redaction_json),
2395
3342
  stepCount: row.step_count,
@@ -2414,6 +3361,7 @@ function mapAutomationStepRecord(row) {
2414
3361
  durationMs: row.duration_ms ?? undefined,
2415
3362
  tabId: row.tab_id ?? undefined,
2416
3363
  target: parseJsonOrUndefined(row.target_summary_json),
3364
+ diagnostics: parseJsonOrUndefined(row.diagnostics_json),
2417
3365
  redaction: parseJsonOrUndefined(row.redaction_json),
2418
3366
  failure: parseJsonOrUndefined(row.failure_json),
2419
3367
  inputMetadata: parseJsonOrUndefined(row.input_metadata_json),
@@ -2424,6 +3372,109 @@ function mapAutomationStepRecord(row) {
2424
3372
  source: 'automation_steps',
2425
3373
  };
2426
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
+ }
2427
3478
  function formatUrlPath(url) {
2428
3479
  try {
2429
3480
  const parsed = new URL(url);
@@ -2556,15 +3607,25 @@ function resolveViewportDimension(value, axis) {
2556
3607
  }
2557
3608
  return floored;
2558
3609
  }
2559
- class WorkflowTargetResolutionError extends Error {
2560
- code;
2561
- details;
2562
- constructor(code, message, details) {
2563
- super(message);
2564
- this.name = 'WorkflowTargetResolutionError';
2565
- this.code = code;
2566
- 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;
3621
+ }
3622
+ if (typeof options.candidateCount === 'number') {
3623
+ diagnostics.candidateCount = options.candidateCount;
2567
3624
  }
3625
+ if (Array.isArray(options.sampledCandidates) && options.sampledCandidates.length > 0) {
3626
+ diagnostics.sampledCandidates = options.sampledCandidates;
3627
+ }
3628
+ return diagnostics;
2568
3629
  }
2569
3630
  function resolveOptionalMatcherString(value) {
2570
3631
  if (typeof value !== 'string') {
@@ -2587,10 +3648,10 @@ function resolveOptionalMatcherCount(value, field) {
2587
3648
  return floored;
2588
3649
  }
2589
3650
  function resolvePageStateScope(value) {
2590
- 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') {
2591
3652
  return value;
2592
3653
  }
2593
- 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');
2594
3655
  }
2595
3656
  function resolvePageStateMatcher(input) {
2596
3657
  const matcher = {
@@ -2600,6 +3661,13 @@ function resolvePageStateMatcher(input) {
2600
3661
  textContains: resolveOptionalMatcherString(input.textContains),
2601
3662
  labelContains: resolveOptionalMatcherString(input.labelContains),
2602
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),
2603
3671
  urlContains: resolveOptionalMatcherString(input.urlContains),
2604
3672
  language: resolveOptionalMatcherString(input.language),
2605
3673
  disabled: resolveOptionalMatcherBoolean(input.disabled),
@@ -2610,6 +3678,7 @@ function resolvePageStateMatcher(input) {
2610
3678
  requiredField: resolveOptionalMatcherBoolean(input.requiredField),
2611
3679
  tagName: resolveOptionalMatcherString(input.tagName)?.toLowerCase(),
2612
3680
  type: resolveOptionalMatcherString(input.type)?.toLowerCase(),
3681
+ visible: resolveOptionalMatcherBoolean(input.visible),
2613
3682
  countExactly: resolveOptionalMatcherCount(input.countExactly, 'countExactly'),
2614
3683
  countAtLeast: resolveOptionalMatcherCount(input.countAtLeast, 'countAtLeast'),
2615
3684
  };
@@ -2624,6 +3693,19 @@ function includesNormalized(value, needle) {
2624
3693
  }
2625
3694
  return typeof value === 'string' && value.toLowerCase().includes(needle.toLowerCase());
2626
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
+ }
2627
3709
  function equalsNormalized(value, expected) {
2628
3710
  if (!expected) {
2629
3711
  return true;
@@ -2637,7 +3719,7 @@ function equalsOptionalBoolean(value, expected) {
2637
3719
  return value === expected;
2638
3720
  }
2639
3721
  function pickPageStateScopeItems(payload, scope) {
2640
- if (scope === 'buttons' || scope === 'inputs' || scope === 'modals') {
3722
+ if (scope === 'buttons' || scope === 'links' || scope === 'inputs' || scope === 'modals') {
2641
3723
  const value = payload[scope];
2642
3724
  return asRecordArray(value);
2643
3725
  }
@@ -2650,13 +3732,20 @@ function pickPageStateScopeItems(payload, scope) {
2650
3732
  function matchesPageStateItem(item, matcher) {
2651
3733
  return (includesNormalized(item.selector, matcher.selector)
2652
3734
  && equalsNormalized(item.testId, matcher.testId)
2653
- && includesNormalized(item.text, matcher.textContains)
2654
- && includesNormalized(item.label, matcher.labelContains)
2655
- && 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)
2656
3744
  && includesNormalized(item.url, matcher.urlContains)
2657
3745
  && equalsNormalized(item.language, matcher.language)
2658
3746
  && equalsNormalized(item.tagName, matcher.tagName)
2659
3747
  && equalsNormalized(item.type, matcher.type)
3748
+ && equalsOptionalBoolean(item.visible, matcher.visible)
2660
3749
  && equalsOptionalBoolean(item.disabled, matcher.disabled)
2661
3750
  && equalsOptionalBoolean(item.selected, matcher.selected)
2662
3751
  && equalsOptionalBoolean(item.pressed, matcher.pressed)
@@ -2713,7 +3802,7 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2713
3802
  const previousSummary = previous?.summary;
2714
3803
  const currentSummary = current.summary;
2715
3804
  const summaryDelta = {};
2716
- for (const key of ['buttons', 'inputs', 'modals']) {
3805
+ for (const key of ['buttons', 'links', 'inputs', 'modals']) {
2717
3806
  const previousValue = typeof previousSummary?.[key] === 'number' ? previousSummary[key] : undefined;
2718
3807
  const currentValue = typeof currentSummary?.[key] === 'number' ? currentSummary[key] : undefined;
2719
3808
  if (previousValue !== currentValue && currentValue !== undefined) {
@@ -2742,13 +3831,13 @@ function createPageChangeSummary(previousCapture, currentCapture) {
2742
3831
  }
2743
3832
  function resolveInteractiveKinds(value) {
2744
3833
  if (!Array.isArray(value) || value.length === 0) {
2745
- return ['buttons', 'inputs', 'modals', 'focused'];
3834
+ return ['buttons', 'links', 'inputs', 'modals', 'focused'];
2746
3835
  }
2747
- const allowed = new Set(['buttons', 'inputs', 'modals', 'focused']);
3836
+ const allowed = new Set(['buttons', 'links', 'inputs', 'modals', 'focused']);
2748
3837
  const kinds = value
2749
3838
  .filter((entry) => typeof entry === 'string' && allowed.has(entry))
2750
3839
  .map((entry) => entry);
2751
- 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'];
2752
3841
  }
2753
3842
  function collectInteractiveElementRefs(payload, kinds, maxItems) {
2754
3843
  const refs = [];
@@ -2868,132 +3957,1345 @@ async function waitForPageStateCondition(sessionId, input, capturePageState) {
2868
3957
  const { lastCapture: _lastCapture, ...waited } = detailed;
2869
3958
  return waited;
2870
3959
  }
2871
- function candidateTextForWorkflowTarget(item) {
2872
- return [item.text, item.label, item.title]
2873
- .filter((value) => typeof value === 'string' && value.trim().length > 0)
2874
- .join(' ')
2875
- .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);
4000
+ }
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
+ }
4097
+ return {
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
+ },
4125
+ };
4126
+ }
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
+ }
4182
+ return {
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
+ },
4212
+ };
4213
+ }
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
+ };
2876
4272
  }
2877
- function describeWorkflowTargetCandidate(item) {
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
+ }
2878
4311
  return {
2879
- text: candidateTextForWorkflowTarget(item) || undefined,
2880
- testId: typeof item.testId === 'string' ? item.testId : undefined,
2881
- selector: typeof item.selector === 'string' ? item.selector : undefined,
2882
- tagName: typeof item.tagName === 'string' ? item.tagName : undefined,
2883
- type: typeof item.type === 'string' ? item.type : undefined,
2884
- disabled: typeof item.disabled === 'boolean' ? item.disabled : undefined,
2885
- selected: typeof item.selected === 'boolean' ? item.selected : undefined,
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,
4367
+ },
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;
4474
+ }
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,
4674
+ },
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.',
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
+ },
2886
4996
  };
2887
4997
  }
2888
- function pickWorkflowTargetItems(payload, scope) {
2889
- if (scope) {
2890
- return pickPageStateScopeItems(payload, scope);
2891
- }
2892
- return [
2893
- ...pickPageStateScopeItems(payload, 'buttons'),
2894
- ...pickPageStateScopeItems(payload, 'inputs'),
2895
- ...pickPageStateScopeItems(payload, 'modals'),
2896
- ...pickPageStateScopeItems(payload, 'focused'),
2897
- ];
2898
- }
2899
- function matchesWorkflowActionTarget(item, target) {
2900
- return (equalsNormalized(item.testId, target.testId)
2901
- && includesNormalized(item.text, target.textContains)
2902
- && includesNormalized(item.label, target.labelContains)
2903
- && includesNormalized(item.title, target.titleContains)
2904
- && equalsNormalized(item.tagName, target.tagName)
2905
- && equalsNormalized(item.type, target.type)
2906
- && equalsOptionalBoolean(item.disabled, target.disabled)
2907
- && equalsOptionalBoolean(item.selected, target.selected)
2908
- && equalsOptionalBoolean(item.pressed, target.pressed)
2909
- && equalsOptionalBoolean(item.expanded, target.expanded)
2910
- && equalsOptionalBoolean(item.readOnly, target.readOnly)
2911
- && equalsOptionalBoolean(item.required, target.requiredField)
2912
- && (typeof item.elementRef === 'string' || typeof item.selector === 'string'));
2913
- }
2914
- function summarizeWorkflowTargetMatcher(target) {
2915
- return {
2916
- scope: target.scope,
2917
- selector: target.selector,
2918
- elementRef: target.elementRef,
2919
- testId: target.testId,
2920
- textContains: target.textContains,
2921
- labelContains: target.labelContains,
2922
- titleContains: target.titleContains,
2923
- tagName: target.tagName,
2924
- type: target.type,
2925
- disabled: target.disabled,
2926
- selected: target.selected,
2927
- pressed: target.pressed,
2928
- expanded: target.expanded,
2929
- readOnly: target.readOnly,
2930
- requiredField: target.requiredField,
2931
- };
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
+ }
2932
5041
  }
2933
- async function resolveWorkflowActionTarget(sessionId, target, capturePageState, existingCapture) {
2934
- if (!target) {
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) {
2935
5080
  return {
2936
- resolution: {
2937
- strategy: 'none',
2938
- },
5081
+ sensitiveInputs: [],
5082
+ frameCount: 0,
5083
+ crossOriginFrameCount: 0,
2939
5084
  };
2940
5085
  }
2941
- if (target.elementRef || target.selector) {
2942
- return {
2943
- target: {
2944
- elementRef: target.elementRef,
2945
- selector: target.selector,
2946
- tabId: target.tabId,
2947
- frameId: target.frameId,
2948
- url: target.url,
2949
- },
2950
- resolution: {
2951
- strategy: target.elementRef ? 'elementRef' : 'selector',
2952
- matcher: summarizeWorkflowTargetMatcher(target),
2953
- },
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,
2954
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.',
5145
+ });
2955
5146
  }
2956
- const capture = existingCapture ?? await capturePageState(sessionId, {
2957
- includeButtons: target.scope ? target.scope === 'buttons' : true,
2958
- includeInputs: target.scope ? target.scope === 'inputs' : true,
2959
- includeModals: target.scope ? target.scope === 'modals' : true,
2960
- maxItems: 100,
2961
- maxTextLength: 120,
2962
- });
2963
- const candidates = pickWorkflowTargetItems(capture.payload, target.scope)
2964
- .filter((item) => matchesWorkflowActionTarget(item, target));
2965
- if (candidates.length === 0) {
2966
- throw new WorkflowTargetResolutionError('workflow_target_not_found', 'No interactive element matched the workflow target.', {
2967
- matcher: summarizeWorkflowTargetMatcher(target),
2968
- searchedScope: target.scope ?? 'all-interactive',
2969
- sampledCandidates: pickWorkflowTargetItems(capture.payload, target.scope)
2970
- .slice(0, 5)
2971
- .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}`,
5153
+ });
5154
+ }
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,
2972
5242
  });
2973
5243
  }
2974
- if (candidates.length > 1) {
2975
- throw new WorkflowTargetResolutionError('workflow_target_ambiguous', `Workflow target matched ${candidates.length} elements; refine the matcher.`, {
2976
- matcher: summarizeWorkflowTargetMatcher(target),
2977
- matchedCandidateCount: candidates.length,
2978
- sampledCandidates: candidates.slice(0, 5).map((item) => describeWorkflowTargetCandidate(item)),
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,
2979
5252
  });
2980
5253
  }
2981
- const candidate = candidates[0];
5254
+ const ready = blockers.length === 0;
2982
5255
  return {
2983
- target: {
2984
- elementRef: typeof candidate.elementRef === 'string' ? candidate.elementRef : undefined,
2985
- selector: typeof candidate.selector === 'string' ? candidate.selector : undefined,
2986
- tabId: target.tabId,
2987
- frameId: target.frameId,
2988
- url: target.url,
2989
- },
2990
- resolution: {
2991
- strategy: typeof candidate.elementRef === 'string' ? 'semantic_elementRef' : 'semantic_selector',
2992
- matcher: summarizeWorkflowTargetMatcher(target),
2993
- matchedCandidateCount: candidates.length,
2994
- matched: describeWorkflowTargetCandidate(candidate),
2995
- },
2996
- 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
+ })),
2997
5299
  };
2998
5300
  }
2999
5301
  function createWorkflowStepId(step, index) {
@@ -3004,6 +5306,7 @@ async function captureWorkflowPageState(sessionId, capturePageState, mode) {
3004
5306
  const maxTextLength = mode === 'fast' ? 60 : 80;
3005
5307
  return capturePageState(sessionId, {
3006
5308
  includeButtons: true,
5309
+ includeLinks: true,
3007
5310
  includeInputs: true,
3008
5311
  includeModals: true,
3009
5312
  maxItems,
@@ -3058,6 +5361,20 @@ function resolveWorkflowRecommendedAction(error) {
3058
5361
  if (error.code === 'page_state_not_matched' || error.code === 'page_state_assertion_failed') {
3059
5362
  return 'inspect_page_state';
3060
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
+ }
3061
5378
  return undefined;
3062
5379
  }
3063
5380
  function resolveWorkflowFailureSelector(step, stepResultTarget) {
@@ -3156,6 +5473,66 @@ function normalizeCaptureError(sessionId, error) {
3156
5473
  }
3157
5474
  return fallback;
3158
5475
  }
5476
+ function isCaptureTimeoutMessage(message) {
5477
+ const normalized = message.toLowerCase();
5478
+ return normalized.includes('timed out') || normalized.includes('timeout');
5479
+ }
5480
+ function isRecoverableOverrideLiveCommandError(error) {
5481
+ if (isLiveSessionDisconnectedError(error)) {
5482
+ return true;
5483
+ }
5484
+ const message = error instanceof Error ? error.message : String(error);
5485
+ return isCaptureTimeoutMessage(message);
5486
+ }
5487
+ function extractTimeoutMsFromMessage(message, fallback) {
5488
+ const match = message.match(/(?:after|waiting)\s+(\d+)ms/i);
5489
+ if (!match) {
5490
+ return fallback;
5491
+ }
5492
+ const parsed = Number.parseInt(match[1] ?? '', 10);
5493
+ return Number.isFinite(parsed) ? parsed : fallback;
5494
+ }
5495
+ function buildOverrideLiveCommandFailure(options) {
5496
+ const originalMessage = options.error instanceof Error ? options.error.message : String(options.error);
5497
+ const timeout = extractTimeoutMsFromMessage(originalMessage, options.timeoutMs);
5498
+ const timedOut = isCaptureTimeoutMessage(originalMessage);
5499
+ const disconnected = isLiveSessionDisconnectedError(options.error);
5500
+ const sessionState = options.getSessionConnectionState?.(options.sessionId);
5501
+ const code = disconnected
5502
+ ? LIVE_SESSION_DISCONNECTED_CODE
5503
+ : timedOut
5504
+ ? OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE
5505
+ : OVERRIDE_LIVE_COMMAND_FAILED_CODE;
5506
+ const message = timedOut
5507
+ ? `${options.command} for session ${options.sessionId} timed out after ${timeout}ms before the live extension returned an override command result.`
5508
+ : disconnected
5509
+ ? `${options.command} for session ${options.sessionId} could not reach a connected live extension target.`
5510
+ : `${options.command} for session ${options.sessionId} failed before returning an override command result.`;
5511
+ return {
5512
+ ok: false,
5513
+ available: false,
5514
+ code,
5515
+ command: options.command,
5516
+ timeoutMs: timeout,
5517
+ timedOut,
5518
+ disconnected,
5519
+ message,
5520
+ originalMessage,
5521
+ sessionConnected: sessionState?.connected,
5522
+ disconnectedAt: sessionState?.disconnectedAt,
5523
+ disconnectReason: sessionState?.disconnectReason,
5524
+ };
5525
+ }
5526
+ function createOverrideLiveCommandError(options) {
5527
+ const failure = buildOverrideLiveCommandFailure(options);
5528
+ const code = String(failure.code ?? OVERRIDE_LIVE_COMMAND_FAILED_CODE);
5529
+ const message = `${code}: ${options.command} for session ${options.sessionId} ${failure.timedOut === true
5530
+ ? `timed out after ${String(failure.timeoutMs)}ms`
5531
+ : `failed`}. ${String(failure.message ?? '')} Original error: ${String(failure.originalMessage ?? 'unknown')}`;
5532
+ const error = new Error(message);
5533
+ Object.assign(error, { code, details: failure });
5534
+ return error;
5535
+ }
3159
5536
  function isLiveSessionDisconnectedError(error) {
3160
5537
  return error instanceof LiveSessionDisconnectedError;
3161
5538
  }
@@ -3167,12 +5544,99 @@ async function executeLiveCapture(captureClient, sessionId, command, payload, ti
3167
5544
  throw normalizeCaptureError(sessionId, error);
3168
5545
  }
3169
5546
  }
5547
+ async function executeOverrideLiveCaptureWithDiagnostics(options) {
5548
+ try {
5549
+ const capture = await executeLiveCapture(options.captureClient, options.sessionId, options.command, options.payload, options.timeoutMs);
5550
+ return { capture, payload: ensureCaptureSuccess(capture, options.sessionId) };
5551
+ }
5552
+ catch (error) {
5553
+ throw createOverrideLiveCommandError({
5554
+ sessionId: options.sessionId,
5555
+ command: options.command,
5556
+ timeoutMs: options.timeoutMs,
5557
+ error,
5558
+ getSessionConnectionState: options.getSessionConnectionState,
5559
+ });
5560
+ }
5561
+ }
3170
5562
  function ensureCaptureSuccess(result, sessionId) {
3171
5563
  if (!result.ok) {
3172
5564
  throw normalizeCaptureError(sessionId, new Error(result.error ?? 'Capture command failed'));
3173
5565
  }
3174
5566
  return result.payload ?? {};
3175
5567
  }
5568
+ async function refreshObservedAssetsForOverrideEnable(options) {
5569
+ const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
5570
+ captureClient: options.captureClient,
5571
+ sessionId: options.sessionId,
5572
+ command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
5573
+ payload: { tabId: options.tabId, includePerformance: true },
5574
+ timeoutMs: 5_000,
5575
+ getSessionConnectionState: options.getSessionConnectionState,
5576
+ });
5577
+ persistObservedOverrideAssets(options.db, {
5578
+ ...payload,
5579
+ sessionId: options.sessionId,
5580
+ tabId: payload.tabId ?? options.tabId,
5581
+ });
5582
+ return {
5583
+ tabId: typeof payload.tabId === 'number' ? payload.tabId : options.tabId,
5584
+ pageUrl: typeof payload.pageUrl === 'string' ? payload.pageUrl : undefined,
5585
+ assetCount: Array.isArray(payload.assets) ? payload.assets.length : 0,
5586
+ };
5587
+ }
5588
+ function buildPersistedOverrideStatus(options) {
5589
+ let profile = null;
5590
+ let profileError;
5591
+ try {
5592
+ profile = resolveOverrideProfileRecord(options.profileId);
5593
+ }
5594
+ catch (error) {
5595
+ profileError = error instanceof Error ? error.message : String(error);
5596
+ }
5597
+ const latestRun = listOverridePocRuns(options.db, options.sessionId, 1, 0).runs[0] ?? null;
5598
+ const recentRequests = listOverridePocRequests(options.db, options.sessionId, 5, 0, latestRun?.runId).requests;
5599
+ const recentPlans = listOverridePlanAudits(options.db, { sessionId: options.sessionId, limit: 5, offset: 0 }).plans;
5600
+ let preflight;
5601
+ if (profileError) {
5602
+ preflight = {
5603
+ ready: false,
5604
+ profileId: null,
5605
+ profile: null,
5606
+ issues: [{
5607
+ code: 'OVERRIDE_CONFIG_UNAVAILABLE',
5608
+ severity: 'error',
5609
+ source: 'profile',
5610
+ message: profileError,
5611
+ }],
5612
+ checks: {
5613
+ sessionFound: auditSessionExists(options.db, options.sessionId),
5614
+ connected: options.getSessionConnectionState?.(options.sessionId)?.connected === true,
5615
+ },
5616
+ nextActions: [{
5617
+ code: 'FIX_OVERRIDE_CONFIG_PATH',
5618
+ message: 'Create a readable override-poc config or point OVERRIDE_POC_CONFIG_PATH at the intended config, then retry override status.',
5619
+ }],
5620
+ };
5621
+ }
5622
+ else {
5623
+ preflight = buildOverridePreflight({
5624
+ db: options.db,
5625
+ sessionId: options.sessionId,
5626
+ profileId: options.profileId,
5627
+ getSessionConnectionState: options.getSessionConnectionState,
5628
+ });
5629
+ }
5630
+ return {
5631
+ profile,
5632
+ profileError,
5633
+ latestRun,
5634
+ recentRequests,
5635
+ recentPlans,
5636
+ preflight,
5637
+ diagnosis: diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId),
5638
+ };
5639
+ }
3176
5640
  function auditSessionExists(db, sessionId) {
3177
5641
  const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1').get(sessionId);
3178
5642
  return row !== undefined;
@@ -3655,15 +6119,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3655
6119
  recommendedAction,
3656
6120
  };
3657
6121
  },
3658
- list_override_profiles: async () => {
6122
+ list_override_profiles: async (input) => {
3659
6123
  const profiles = buildOverrideProfileRecords();
6124
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
3660
6125
  return {
3661
6126
  ...createBaseResponse(),
3662
6127
  limitsApplied: {
3663
6128
  maxResults: profiles.length,
3664
6129
  truncated: false,
3665
6130
  },
3666
- profiles,
6131
+ responseProfile,
6132
+ profiles: profiles.map((profile) => serializeOverrideProfile(profile, responseProfile)),
3667
6133
  nextActions: profiles.length > 0
3668
6134
  ? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
3669
6135
  : [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
@@ -3701,6 +6167,8 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3701
6167
  });
3702
6168
  const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
3703
6169
  const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
6170
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
6171
+ const includeConfigJson = input.includeConfigJson === true || responseProfile === 'full';
3704
6172
  const write = {
3705
6173
  written: false,
3706
6174
  path: generated.suggestedConfigPath,
@@ -3749,21 +6217,31 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3749
6217
  warnings: generated.warnings,
3750
6218
  nextActions,
3751
6219
  write,
3752
- profile: generated.profile,
3753
- config: generated.config,
3754
- 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,
3755
6231
  };
3756
6232
  },
3757
6233
  validate_override_profile: async (input) => {
3758
6234
  const profile = resolveOverrideProfileRecord(input.profileId);
3759
6235
  const issues = buildOverrideProfileIssues(profile);
6236
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
3760
6237
  return {
3761
6238
  ...createBaseResponse(),
3762
6239
  profileId: profile.profileId,
3763
6240
  valid: !issues.some((issue) => issue.severity === 'error'),
3764
6241
  issues,
3765
6242
  nextActions: buildOverrideProfileNextActions(profile, issues),
3766
- profile,
6243
+ responseProfile,
6244
+ profile: serializeOverrideProfile(profile, responseProfile),
3767
6245
  };
3768
6246
  },
3769
6247
  preflight_overrides: async (input) => {
@@ -3790,16 +6268,18 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
3790
6268
  }
3791
6269
  const assets = listObservedOverrideAssets(getDb(), {
3792
6270
  sessionId,
3793
- limit: typeof input.limit === 'number' ? input.limit : undefined,
6271
+ limit: typeof input.limit === 'number' ? input.limit : 50,
3794
6272
  sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
3795
6273
  });
6274
+ const responseProfile = resolveOverrideResponseProfile(input.responseProfile);
3796
6275
  return {
3797
6276
  ...createBaseResponse(sessionId),
3798
6277
  limitsApplied: {
3799
6278
  maxResults: assets.length,
3800
6279
  truncated: false,
3801
6280
  },
3802
- assets,
6281
+ responseProfile,
6282
+ assets: responseProfile === 'full' ? assets : assets.map(compactObservedOverrideAsset),
3803
6283
  };
3804
6284
  },
3805
6285
  plan_override_response_patch: async (input) => {
@@ -5247,9 +7727,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5247
7727
  r.status,
5248
7728
  r.started_at,
5249
7729
  r.completed_at,
5250
- r.stop_reason,
5251
- r.target_summary_json,
5252
- r.failure_json,
7730
+ r.stop_reason,
7731
+ r.target_summary_json,
7732
+ r.diagnostics_json,
7733
+ r.failure_json,
5253
7734
  r.redaction_json,
5254
7735
  r.created_at,
5255
7736
  r.updated_at,
@@ -5311,9 +7792,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5311
7792
  r.status,
5312
7793
  r.started_at,
5313
7794
  r.completed_at,
5314
- r.stop_reason,
5315
- r.target_summary_json,
5316
- r.failure_json,
7795
+ r.stop_reason,
7796
+ r.target_summary_json,
7797
+ r.diagnostics_json,
7798
+ r.failure_json,
5317
7799
  r.redaction_json,
5318
7800
  r.created_at,
5319
7801
  r.updated_at,
@@ -5345,9 +7827,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
5345
7827
  started_at,
5346
7828
  finished_at,
5347
7829
  duration_ms,
5348
- tab_id,
5349
- target_summary_json,
5350
- redaction_json,
7830
+ tab_id,
7831
+ target_summary_json,
7832
+ diagnostics_json,
7833
+ redaction_json,
5351
7834
  failure_json,
5352
7835
  input_metadata_json,
5353
7836
  event_type,
@@ -5381,12 +7864,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5381
7864
  const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
5382
7865
  const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
5383
7866
  const includeButtons = input.includeButtons !== false;
7867
+ const includeLinks = input.includeLinks !== false;
5384
7868
  const includeInputs = input.includeInputs !== false;
5385
7869
  const includeModals = input.includeModals !== false;
5386
7870
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_PAGE_STATE', {
5387
7871
  maxItems,
5388
7872
  maxTextLength,
5389
7873
  includeButtons,
7874
+ includeLinks,
5390
7875
  includeInputs,
5391
7876
  includeModals,
5392
7877
  }, 4_000);
@@ -5405,8 +7890,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5405
7890
  throw new Error('sessionId is required');
5406
7891
  }
5407
7892
  const tabId = resolveOptionalTabId(input.tabId);
5408
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: input.includePerformance !== false }, 5_000);
5409
- const payload = ensureCaptureSuccess(capture, sessionId);
7893
+ const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
7894
+ captureClient,
7895
+ sessionId,
7896
+ command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
7897
+ payload: { tabId, includePerformance: input.includePerformance !== false },
7898
+ timeoutMs: 5_000,
7899
+ getSessionConnectionState,
7900
+ });
5410
7901
  const assetCount = Array.isArray(payload.assets) ? payload.assets.length : 0;
5411
7902
  const persisted = getDb
5412
7903
  ? persistObservedOverrideAssets(getDb(), { ...payload, sessionId, tabId: payload.tabId ?? tabId })
@@ -5436,23 +7927,31 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5436
7927
  assertOverrideResponseRequestCaptureSafe({
5437
7928
  requestMethod: input.requestMethod,
5438
7929
  requestHeaders: input.requestHeaders,
7930
+ ruleType: input.ruleType,
5439
7931
  subject: 'Response body capture request',
5440
7932
  });
5441
7933
  const tabId = resolveOptionalTabId(input.tabId);
5442
7934
  const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
5443
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_RESPONSE_BODY', {
5444
- targetUrl,
5445
- tabId,
5446
- captureMode: normalizeOptionalString(input.captureMode),
5447
- triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
5448
- matchMode: normalizeOptionalString(input.matchMode),
5449
- requestMethod: input.requestMethod,
5450
- requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
5451
- timeoutMs,
5452
- maxBodyBytes: input.maxBodyBytes,
5453
- includeBody: input.includeBody === true,
5454
- }, timeoutMs + 2_000);
5455
- const payload = ensureCaptureSuccess(capture, sessionId);
7935
+ const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
7936
+ captureClient,
7937
+ sessionId,
7938
+ command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
7939
+ payload: {
7940
+ targetUrl,
7941
+ tabId,
7942
+ captureMode: normalizeOptionalString(input.captureMode),
7943
+ triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
7944
+ matchMode: normalizeOptionalString(input.matchMode),
7945
+ ruleType: normalizeOptionalString(input.ruleType),
7946
+ requestMethod: input.requestMethod,
7947
+ requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
7948
+ timeoutMs,
7949
+ maxBodyBytes: input.maxBodyBytes,
7950
+ includeBody: input.includeBody === true,
7951
+ },
7952
+ timeoutMs: timeoutMs + 2_000,
7953
+ getSessionConnectionState,
7954
+ });
5456
7955
  return {
5457
7956
  ...createBaseResponse(sessionId),
5458
7957
  limitsApplied: {
@@ -5480,19 +7979,26 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5480
7979
  }
5481
7980
  const tabId = resolveOptionalTabId(input.tabId);
5482
7981
  const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
5483
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_RESPONSE_BODY', {
5484
- targetUrl,
5485
- tabId,
5486
- captureMode: normalizeOptionalString(input.captureMode),
5487
- triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
5488
- matchMode: normalizeOptionalString(input.matchMode),
5489
- requestMethod: input.requestMethod,
5490
- requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
5491
- timeoutMs,
5492
- maxBodyBytes: input.maxBodyBytes,
5493
- includeBody: true,
5494
- }, timeoutMs + 2_000);
5495
- const payload = ensureCaptureSuccess(capture, sessionId);
7982
+ const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
7983
+ captureClient,
7984
+ sessionId,
7985
+ command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
7986
+ payload: {
7987
+ targetUrl,
7988
+ tabId,
7989
+ captureMode: normalizeOptionalString(input.captureMode),
7990
+ triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
7991
+ matchMode: normalizeOptionalString(input.matchMode),
7992
+ ruleType: normalizeOptionalString(input.ruleType),
7993
+ requestMethod: input.requestMethod,
7994
+ requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
7995
+ timeoutMs,
7996
+ maxBodyBytes: input.maxBodyBytes,
7997
+ includeBody: true,
7998
+ },
7999
+ timeoutMs: timeoutMs + 2_000,
8000
+ getSessionConnectionState,
8001
+ });
5496
8002
  if (payload.truncated === true) {
5497
8003
  throw new Error('Captured response body was truncated; increase maxBodyBytes before planning a patch.');
5498
8004
  }
@@ -5585,8 +8091,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5585
8091
  let observedFromPersisted;
5586
8092
  if (!Array.isArray(observedAssets) && sessionId) {
5587
8093
  const tabId = resolveOptionalTabId(input.tabId);
8094
+ const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
8095
+ const timeoutMs = 5_000;
5588
8096
  try {
5589
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: true }, 5_000);
8097
+ const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
5590
8098
  observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
5591
8099
  observedAssets = observedFromLiveTab.assets;
5592
8100
  if (getDb) {
@@ -5594,11 +8102,22 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5594
8102
  }
5595
8103
  }
5596
8104
  catch (error) {
5597
- if (!getDb || !isLiveSessionDisconnectedError(error)) {
8105
+ if (getDb && isRecoverableOverrideLiveCommandError(error)) {
8106
+ observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
8107
+ observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
8108
+ }
8109
+ else if (isRecoverableOverrideLiveCommandError(error)) {
8110
+ throw createOverrideLiveCommandError({
8111
+ sessionId,
8112
+ command,
8113
+ timeoutMs,
8114
+ error,
8115
+ getSessionConnectionState,
8116
+ });
8117
+ }
8118
+ else {
5598
8119
  throw error;
5599
8120
  }
5600
- observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
5601
- observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
5602
8121
  }
5603
8122
  }
5604
8123
  const mapping = await mapNextOverrideAssetsWithDrift({
@@ -5642,8 +8161,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5642
8161
  let observedFromPersisted;
5643
8162
  if (!Array.isArray(observedAssets) && sessionId) {
5644
8163
  const tabId = resolveOptionalTabId(input.tabId);
8164
+ const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
8165
+ const timeoutMs = 5_000;
5645
8166
  try {
5646
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_OBSERVE_ASSETS', { tabId, includePerformance: true }, 5_000);
8167
+ const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
5647
8168
  observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
5648
8169
  observedAssets = observedFromLiveTab.assets;
5649
8170
  if (getDb) {
@@ -5651,11 +8172,22 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5651
8172
  }
5652
8173
  }
5653
8174
  catch (error) {
5654
- if (!getDb || !isLiveSessionDisconnectedError(error)) {
8175
+ if (getDb && isRecoverableOverrideLiveCommandError(error)) {
8176
+ observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
8177
+ observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
8178
+ }
8179
+ else if (isRecoverableOverrideLiveCommandError(error)) {
8180
+ throw createOverrideLiveCommandError({
8181
+ sessionId,
8182
+ command,
8183
+ timeoutMs,
8184
+ error,
8185
+ getSessionConnectionState,
8186
+ });
8187
+ }
8188
+ else {
5655
8189
  throw error;
5656
8190
  }
5657
- observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
5658
- observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
5659
8191
  }
5660
8192
  }
5661
8193
  const plan = await planNextSourceOverride({
@@ -5716,14 +8248,58 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5716
8248
  if (!sessionId) {
5717
8249
  throw new Error('sessionId is required');
5718
8250
  }
5719
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_GET_STATUS', {}, 3_000);
5720
- const payload = ensureCaptureSuccess(capture, sessionId);
8251
+ const command = 'CAPTURE_OVERRIDE_POC_GET_STATUS';
8252
+ const timeoutMs = 3_000;
8253
+ let capture;
8254
+ let payload;
8255
+ try {
8256
+ capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
8257
+ payload = ensureCaptureSuccess(capture, sessionId);
8258
+ }
8259
+ catch (error) {
8260
+ if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
8261
+ throw error;
8262
+ }
8263
+ const persisted = buildPersistedOverrideStatus({
8264
+ db: getDb(),
8265
+ sessionId,
8266
+ profileId: input.profileId,
8267
+ getSessionConnectionState,
8268
+ });
8269
+ const liveStatus = buildOverrideLiveCommandFailure({
8270
+ sessionId,
8271
+ command,
8272
+ timeoutMs,
8273
+ error,
8274
+ getSessionConnectionState,
8275
+ });
8276
+ return {
8277
+ ...createBaseResponse(sessionId),
8278
+ limitsApplied: {
8279
+ maxResults: 1,
8280
+ truncated: false,
8281
+ },
8282
+ statusSource: 'persisted-audit',
8283
+ liveStatus,
8284
+ ...persisted,
8285
+ nextActions: [
8286
+ { code: 'RECONNECT_OR_RETRY_OVERRIDE_STATUS', message: 'Reconnect or rebind the top-level session, then retry get_override_status for live debugger state.' },
8287
+ { code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides to inspect persisted run and readiness signals while live status is unavailable.' },
8288
+ ],
8289
+ };
8290
+ }
5721
8291
  return {
5722
8292
  ...createBaseResponse(sessionId),
5723
8293
  limitsApplied: {
5724
8294
  maxResults: 1,
5725
8295
  truncated: capture.truncated ?? false,
5726
8296
  },
8297
+ statusSource: 'live',
8298
+ liveStatus: {
8299
+ available: true,
8300
+ command,
8301
+ timeoutMs,
8302
+ },
5727
8303
  preflight: getDb
5728
8304
  ? buildOverridePreflight({
5729
8305
  db: getDb(),
@@ -5763,7 +8339,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5763
8339
  if (!sessionId) {
5764
8340
  throw new Error('sessionId is required');
5765
8341
  }
5766
- const preflight = getDb
8342
+ const tabId = resolveOptionalTabId(input.tabId);
8343
+ let preflight = getDb
5767
8344
  ? buildOverridePreflight({
5768
8345
  db: getDb(),
5769
8346
  sessionId,
@@ -5771,20 +8348,58 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5771
8348
  getSessionConnectionState,
5772
8349
  })
5773
8350
  : null;
8351
+ let observedBeforeEnable;
8352
+ let observedAssetRefreshError;
8353
+ const initialBlockingCodes = getBlockingPreflightCodes(preflight);
8354
+ const initialProfile = isRecord(preflight?.profile) ? preflight.profile : {};
8355
+ const initialExperimentalBypass = canBypassPreflightForExperimentalRsc(initialProfile, initialBlockingCodes);
8356
+ if (preflight && getDb && !initialExperimentalBypass && shouldRefreshObservedAssetsForEnable(preflight)) {
8357
+ try {
8358
+ observedBeforeEnable = await refreshObservedAssetsForOverrideEnable({
8359
+ captureClient,
8360
+ db: getDb(),
8361
+ sessionId,
8362
+ tabId,
8363
+ getSessionConnectionState,
8364
+ });
8365
+ preflight = buildOverridePreflight({
8366
+ db: getDb(),
8367
+ sessionId,
8368
+ profileId: input.profileId,
8369
+ getSessionConnectionState,
8370
+ });
8371
+ }
8372
+ catch (error) {
8373
+ observedAssetRefreshError = error instanceof Error ? error.message : String(error);
8374
+ }
8375
+ }
5774
8376
  if (preflight && preflight.ready !== true) {
5775
- const blockingCodes = Array.isArray(preflight.issues)
5776
- ? preflight.issues
5777
- .filter((issue) => isRecord(issue) && issue.severity === 'error')
5778
- .map((issue) => String(issue.code ?? 'UNKNOWN'))
5779
- : [];
8377
+ const blockingCodes = getBlockingPreflightCodes(preflight);
5780
8378
  const profile = isRecord(preflight.profile) ? preflight.profile : {};
5781
8379
  if (!canBypassPreflightForExperimentalRsc(profile, blockingCodes)) {
5782
- throw new Error(`Override preflight failed: ${blockingCodes.join(', ') || 'UNKNOWN'}`);
8380
+ const refreshSuffix = observedAssetRefreshError
8381
+ ? `; observed asset refresh failed: ${observedAssetRefreshError}`
8382
+ : '';
8383
+ throw new Error(`Override preflight failed: ${blockingCodes.join(', ') || 'UNKNOWN'}${refreshSuffix}`);
5783
8384
  }
5784
8385
  }
5785
- const tabId = resolveOptionalTabId(input.tabId);
5786
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_ENABLE', { tabId }, 8_000);
5787
- const payload = ensureCaptureSuccess(capture, sessionId);
8386
+ const command = 'CAPTURE_OVERRIDE_POC_ENABLE';
8387
+ const timeoutMs = 8_000;
8388
+ let capture;
8389
+ let payload;
8390
+ try {
8391
+ capture = await executeLiveCapture(captureClient, sessionId, command, { tabId }, timeoutMs);
8392
+ payload = ensureCaptureSuccess(capture, sessionId);
8393
+ }
8394
+ catch (error) {
8395
+ throw createOverrideLiveCommandError({
8396
+ sessionId,
8397
+ command,
8398
+ timeoutMs,
8399
+ error,
8400
+ getSessionConnectionState,
8401
+ });
8402
+ }
5788
8403
  return {
5789
8404
  ...createBaseResponse(sessionId),
5790
8405
  limitsApplied: {
@@ -5792,6 +8407,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5792
8407
  truncated: capture.truncated ?? false,
5793
8408
  },
5794
8409
  preflight,
8410
+ observedBeforeEnable,
5795
8411
  ...payload,
5796
8412
  nextActions: [{ code: 'RELOAD_OR_INTERACT', message: 'Reload or interact with the tab so configured asset requests occur under the active override.' }],
5797
8413
  };
@@ -5801,14 +8417,64 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5801
8417
  if (!sessionId) {
5802
8418
  throw new Error('sessionId is required');
5803
8419
  }
5804
- const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_OVERRIDE_POC_DISABLE', {}, 5_000);
5805
- const payload = ensureCaptureSuccess(capture, sessionId);
8420
+ const command = 'CAPTURE_OVERRIDE_POC_DISABLE';
8421
+ const timeoutMs = 5_000;
8422
+ let capture;
8423
+ let payload;
8424
+ try {
8425
+ capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
8426
+ payload = ensureCaptureSuccess(capture, sessionId);
8427
+ }
8428
+ catch (error) {
8429
+ if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
8430
+ throw createOverrideLiveCommandError({
8431
+ sessionId,
8432
+ command,
8433
+ timeoutMs,
8434
+ error,
8435
+ getSessionConnectionState,
8436
+ });
8437
+ }
8438
+ const persisted = buildPersistedOverrideStatus({
8439
+ db: getDb(),
8440
+ sessionId,
8441
+ profileId: input.profileId,
8442
+ getSessionConnectionState,
8443
+ });
8444
+ const disableAttempt = buildOverrideLiveCommandFailure({
8445
+ sessionId,
8446
+ command,
8447
+ timeoutMs,
8448
+ error,
8449
+ getSessionConnectionState,
8450
+ });
8451
+ return {
8452
+ ...createBaseResponse(sessionId),
8453
+ limitsApplied: {
8454
+ maxResults: 1,
8455
+ truncated: false,
8456
+ },
8457
+ statusSource: 'persisted-audit',
8458
+ disableAttempt,
8459
+ ...persisted,
8460
+ nextActions: [
8461
+ { code: 'RECONNECT_OR_RETRY_DISABLE', message: 'Reconnect or rebind the top-level session, then retry disable_overrides to confirm debugger detachment.' },
8462
+ { code: 'GET_OVERRIDE_STATUS', message: 'Run get_override_status after reconnecting to verify whether the override is still active.' },
8463
+ ],
8464
+ };
8465
+ }
5806
8466
  return {
5807
8467
  ...createBaseResponse(sessionId),
5808
8468
  limitsApplied: {
5809
8469
  maxResults: 1,
5810
8470
  truncated: capture.truncated ?? false,
5811
8471
  },
8472
+ statusSource: 'live',
8473
+ disableAttempt: {
8474
+ ok: true,
8475
+ command,
8476
+ timeoutMs,
8477
+ },
5812
8478
  ...payload,
5813
8479
  nextActions: [{ code: 'VERIFY_DISABLED', message: 'Run get_override_status if you need to confirm the debugger override is inactive.' }],
5814
8480
  };
@@ -5880,7 +8546,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5880
8546
  throw new Error('selector is required');
5881
8547
  }
5882
8548
  const properties = asStringArray(input.properties, 64);
5883
- 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);
5884
8553
  return {
5885
8554
  ...createBaseResponse(sessionId),
5886
8555
  limitsApplied: {
@@ -5896,7 +8565,10 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5896
8565
  throw new Error('sessionId is required');
5897
8566
  }
5898
8567
  const selector = typeof input.selector === 'string' ? input.selector : undefined;
5899
- 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);
5900
8572
  return {
5901
8573
  ...createBaseResponse(sessionId),
5902
8574
  limitsApplied: {
@@ -5927,6 +8599,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
5927
8599
  const normalizedInput = {
5928
8600
  ...input,
5929
8601
  includeButtons: kinds.includes('buttons'),
8602
+ includeLinks: kinds.includes('links'),
5930
8603
  includeInputs: kinds.includes('inputs'),
5931
8604
  includeModals: kinds.includes('modals'),
5932
8605
  };
@@ -6007,6 +8680,247 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6007
8680
  ...waited,
6008
8681
  };
6009
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
+ },
6010
8924
  run_ui_steps: async (input) => {
6011
8925
  const request = RunUIStepsSchema.parse(input);
6012
8926
  const workflowTraceId = createUIWorkflowTraceId();
@@ -6034,7 +8948,16 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6034
8948
  const previousCapture = lastPageCapture;
6035
8949
  try {
6036
8950
  if (step.kind === 'action') {
6037
- 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);
6038
8961
  const liveRequest = LiveUIActionRequestSchema.parse({
6039
8962
  action: step.action,
6040
8963
  target: resolvedTarget.target,
@@ -6045,6 +8968,12 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6045
8968
  const payload = ensureCaptureSuccess(capture, request.sessionId);
6046
8969
  const actionResult = payload;
6047
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;
6048
8977
  let currentCapture = resolvedTarget.pageCapture ?? lastPageCapture;
6049
8978
  if (!failed && request.mode === 'fast') {
6050
8979
  await sleep(75);
@@ -6059,7 +8988,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6059
8988
  action: step.action,
6060
8989
  traceId: actionResult.traceId,
6061
8990
  target: {
6062
- resolution: resolvedTarget.resolution,
8991
+ resolution: nativeLocatorResolution ?? resolvedTarget.resolution,
6063
8992
  actionTarget: typeof actionResult.target === 'object' && actionResult.target !== null
6064
8993
  ? actionResult.target
6065
8994
  : undefined,
@@ -6072,6 +9001,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6072
9001
  : undefined,
6073
9002
  pageChangeSummary: createPageChangeSummary(previousCapture, currentCapture),
6074
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
+ }
6075
9012
  }
6076
9013
  else if (step.kind === 'waitFor') {
6077
9014
  const waitInput = {
@@ -6099,6 +9036,48 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6099
9036
  pageChangeSummary: createPageChangeSummary(previousCapture, waited.lastCapture),
6100
9037
  };
6101
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
+ }
6102
9081
  else {
6103
9082
  const capture = request.mode === 'fast' && lastPageCapture
6104
9083
  ? lastPageCapture
@@ -6133,7 +9112,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6133
9112
  target: step.kind === 'action' && workflowError
6134
9113
  ? workflowError.details
6135
9114
  : undefined,
6136
- 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,
6137
9117
  error: normalizeWorkflowError(error),
6138
9118
  };
6139
9119
  }
@@ -6156,6 +9136,14 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6156
9136
  if (evidence) {
6157
9137
  failureCaptureCount += 1;
6158
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
+ }
6159
9147
  }
6160
9148
  }
6161
9149
  stepResults.push(finalStepResult);
@@ -6175,7 +9163,8 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6175
9163
  status: 'skipped',
6176
9164
  durationMs: 0,
6177
9165
  action: step.kind === 'action' ? step.action : undefined,
6178
- 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,
6179
9168
  pageChangeSummary: undefined,
6180
9169
  error: {
6181
9170
  code: 'workflow_stopped_early',
@@ -6375,7 +9364,62 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6375
9364
  const actionInput = { ...input };
6376
9365
  delete actionInput.sessionId;
6377
9366
  delete actionInput.captureOnFailure;
6378
- 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
+ }
6379
9423
  const failureCaptureOptions = resolveFailureEvidenceCaptureOptions(input);
6380
9424
  const capture = await executeLiveCapture(captureClient, sessionId, 'EXECUTE_UI_ACTION', request, 5_000);
6381
9425
  const payload = ensureCaptureSuccess(capture, sessionId);
@@ -6400,6 +9444,24 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6400
9444
  const target = typeof actionResult.target === 'object' && actionResult.target !== null
6401
9445
  ? actionResult.target
6402
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
+ }
6403
9465
  return {
6404
9466
  ...createBaseResponse(sessionId),
6405
9467
  limitsApplied: {
@@ -6418,6 +9480,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6418
9480
  : undefined,
6419
9481
  actionResult,
6420
9482
  target,
9483
+ targetResolution,
6421
9484
  tabContext: {
6422
9485
  tabId: typeof target.tabId === 'number' ? target.tabId : undefined,
6423
9486
  frameId: typeof target.frameId === 'number' ? target.frameId : 0,
@@ -6428,7 +9491,7 @@ export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionS
6428
9491
  postActionState,
6429
9492
  supportedScopes: {
6430
9493
  executionScope: actionResult.executionScope,
6431
- topDocumentOnly: true,
9494
+ topDocumentOnly: false,
6432
9495
  opensNewBrowserSession: false,
6433
9496
  },
6434
9497
  };
@@ -6480,13 +9543,37 @@ export function createToolRegistry(overrides = {}) {
6480
9543
  };
6481
9544
  });
6482
9545
  }
6483
- export async function routeToolCall(tools, toolName, input) {
9546
+ export async function routeToolCall(tools, toolName, input, options = {}) {
6484
9547
  const tool = tools.find((candidate) => candidate.name === toolName);
6485
9548
  if (!tool) {
6486
9549
  throw new Error(`Unknown tool: ${toolName}`);
6487
9550
  }
6488
- const response = await tool.handler(isRecord(input) ? input : {});
6489
- 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
+ }
6490
9577
  }
6491
9578
  export function createMCPServer(overrides = {}, options = {}) {
6492
9579
  const logger = options.logger ?? createDefaultMcpLogger();
@@ -6498,6 +9585,17 @@ export function createMCPServer(overrides = {}, options = {}) {
6498
9585
  ...v2Handlers,
6499
9586
  ...overrides,
6500
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
+ });
6501
9599
  const server = new Server({
6502
9600
  name: 'browser-debug-mcp-bridge',
6503
9601
  version: '1.0.0',
@@ -6521,7 +9619,7 @@ export function createMCPServer(overrides = {}, options = {}) {
6521
9619
  const startedAt = Date.now();
6522
9620
  logger.info({ component: 'mcp', event: 'tool_call_started', toolName }, '[MCPServer][MCP] Tool call started');
6523
9621
  try {
6524
- const response = await routeToolCall(tools, toolName, request.params.arguments);
9622
+ const response = await routeToolCall(tools, toolName, request.params.arguments, { loopGuard });
6525
9623
  logger.info({
6526
9624
  component: 'mcp',
6527
9625
  event: 'tool_call_completed',