@reconcrap/boss-recommend-mcp 2.0.7 → 2.0.9

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.
@@ -24,6 +24,11 @@ import {
24
24
  } from "../../core/infinite-list/index.js";
25
25
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
26
26
  import { createRunLifecycleManager } from "../../core/run/index.js";
27
+ import {
28
+ addTiming,
29
+ imageEvidenceFilePath,
30
+ measureTiming
31
+ } from "../../core/run/timing.js";
27
32
  import {
28
33
  callScreeningLlm,
29
34
  normalizeText,
@@ -369,7 +374,8 @@ export async function runChatWorkflow({
369
374
  listStableSignatureLimit = 2,
370
375
  listWheelDeltaY = 850,
371
376
  listSettleMs = 1200,
372
- listFallbackPoint = null
377
+ listFallbackPoint = null,
378
+ imageOutputDir = ""
373
379
  } = {}, runControl) {
374
380
  if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
375
381
  const normalizedDetailSource = normalizeDetailSource(detailSource);
@@ -584,6 +590,8 @@ export async function runChatWorkflow({
584
590
  || results.filter((item) => item.screening?.passed).length < passTarget
585
591
  )
586
592
  ) {
593
+ const candidateStarted = Date.now();
594
+ const timings = {};
587
595
  await runControl.waitIfPaused();
588
596
  runControl.throwIfCanceled();
589
597
  runControl.setPhase("chat:candidate");
@@ -596,7 +604,7 @@ export async function runChatWorkflow({
596
604
  continue;
597
605
  }
598
606
 
599
- const nextCandidateResult = await getNextInfiniteListCandidate({
607
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
600
608
  client,
601
609
  state: listState,
602
610
  maxScrolls: listMaxScrolls,
@@ -623,7 +631,7 @@ export async function runChatWorkflow({
623
631
  visible_index: visibleIndex
624
632
  }
625
633
  })
626
- });
634
+ }));
627
635
  if (!nextCandidateResult.ok) {
628
636
  const endTopLevelState = await getChatTopLevelState(client);
629
637
  if (!endTopLevelState.is_chat_shell) {
@@ -665,11 +673,11 @@ export async function runChatWorkflow({
665
673
 
666
674
  detailStep = "select_candidate";
667
675
  networkRecorder.clear();
668
- const selected = await selectFreshChatCandidate(client, {
676
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
669
677
  cardNodeId,
670
678
  candidate: cardCandidate,
671
679
  timeoutMs: onlineResumeButtonTimeoutMs
672
- });
680
+ }));
673
681
  if (selected.ready?.forbidden_top_level_navigation) {
674
682
  throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
675
683
  }
@@ -696,13 +704,13 @@ export async function runChatWorkflow({
696
704
  if (!detailResult) {
697
705
  detailStep = "open_online_resume";
698
706
  networkRecorder.clear();
699
- const openedResume = await openChatOnlineResume(client, {
707
+ const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
700
708
  timeoutMs: readyTimeoutMs
701
- });
709
+ }));
702
710
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
703
711
  detailStep = "wait_network";
704
712
  const networkWait = ["network", "cascade"].includes(normalizedDetailSource)
705
- ? await waitForCvNetworkEvents(
713
+ ? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
706
714
  waitForChatProfileNetworkEvents,
707
715
  networkRecorder,
708
716
  {
@@ -711,8 +719,11 @@ export async function runChatWorkflow({
711
719
  requireLoaded: true,
712
720
  intervalMs: 200
713
721
  }
714
- )
722
+ ))
715
723
  : null;
724
+ if (networkWait?.elapsed_ms != null) {
725
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
726
+ }
716
727
  let contentWait = {
717
728
  ok: false,
718
729
  skipped: false,
@@ -741,8 +752,11 @@ export async function runChatWorkflow({
741
752
  resumeNetworkEvents
742
753
  ),
743
754
  targetUrl,
744
- closeResume: false
755
+ closeResume: false,
756
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
757
+ networkParseIntervalMs: 250
745
758
  });
759
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
746
760
  parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
747
761
  if (parsedNetworkProfileCount > 0) {
748
762
  contentWait = {
@@ -759,10 +773,10 @@ export async function runChatWorkflow({
759
773
 
760
774
  if (!detailResult) {
761
775
  detailStep = "wait_resume_content";
762
- contentWait = await waitForChatResumeContent(client, {
776
+ contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
763
777
  timeoutMs: resumeDomTimeoutMs,
764
778
  intervalMs: 300
765
- });
779
+ }));
766
780
  resumeState = contentWait.resume_state || openedResume.resume_state;
767
781
  resumeHtml = contentWait.resume_html || null;
768
782
  resumeNetworkEvents = networkRecorder.events.slice();
@@ -778,8 +792,11 @@ export async function runChatWorkflow({
778
792
  resumeNetworkEvents
779
793
  ),
780
794
  targetUrl,
781
- closeResume: false
795
+ closeResume: false,
796
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
797
+ networkParseIntervalMs: 250
782
798
  });
799
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
783
800
  parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
784
801
  }
785
802
 
@@ -792,11 +809,25 @@ export async function runChatWorkflow({
792
809
  if (shouldCaptureImage) {
793
810
  if (captureNodeId) {
794
811
  detailStep = "capture_image_fallback";
795
- imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
812
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
813
+ filePath: imageEvidenceFilePath({
814
+ imageOutputDir,
815
+ domain: "chat",
816
+ runId: runControl?.runId,
817
+ index,
818
+ extension: "jpg"
819
+ }),
820
+ format: "jpeg",
821
+ quality: 72,
822
+ optimize: true,
823
+ resizeMaxWidth: 1100,
824
+ captureViewport: true,
796
825
  padding: 8,
797
826
  maxScreenshots: maxImagePages,
798
827
  wheelDeltaY: imageWheelDeltaY,
799
- settleMs: 1200,
828
+ settleMs: 350,
829
+ duplicateStopCount: 1,
830
+ skipDuplicateScreenshots: true,
800
831
  metadata: {
801
832
  domain: "chat",
802
833
  capture_mode: "scroll_sequence",
@@ -806,7 +837,7 @@ export async function runChatWorkflow({
806
837
  run_candidate_index: index,
807
838
  candidate_key: candidateKey
808
839
  }
809
- });
840
+ }));
810
841
  source = "image";
811
842
  recordCvImageFallback(cvAcquisitionState, {
812
843
  parsedNetworkProfileCount,
@@ -819,7 +850,7 @@ export async function runChatWorkflow({
819
850
  llmResult = createMissingLlmConfigResult();
820
851
  } else {
821
852
  try {
822
- llmResult = await callScreeningLlm({
853
+ llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
823
854
  candidate: detailResult.candidate,
824
855
  criteria,
825
856
  config: llmConfig,
@@ -827,7 +858,7 @@ export async function runChatWorkflow({
827
858
  imageEvidence,
828
859
  maxImages: llmImageLimit,
829
860
  imageDetail: llmImageDetail
830
- });
861
+ }));
831
862
  } catch (error) {
832
863
  llmResult = createFailedLlmResult(error);
833
864
  }
@@ -861,7 +892,10 @@ export async function runChatWorkflow({
861
892
  llmResult = createMissingLlmConfigResult();
862
893
  } else {
863
894
  try {
864
- llmResult = await callScreeningLlm({
895
+ const llmTimingKey = imageEvidence?.file_paths?.length
896
+ ? "vision_model_ms"
897
+ : "text_model_ms";
898
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
865
899
  candidate: detailResult.candidate,
866
900
  criteria,
867
901
  config: llmConfig,
@@ -869,7 +903,7 @@ export async function runChatWorkflow({
869
903
  imageEvidence,
870
904
  maxImages: llmImageLimit,
871
905
  imageDetail: llmImageDetail
872
- });
906
+ }));
873
907
  } catch (error) {
874
908
  llmResult = createFailedLlmResult(error);
875
909
  }
@@ -879,7 +913,7 @@ export async function runChatWorkflow({
879
913
  let closeResult = null;
880
914
  if (closeResume) {
881
915
  detailStep = "close_resume_modal";
882
- closeResult = await closeChatResumeModal(client);
916
+ closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
883
917
  }
884
918
  detailResult.close_result = closeResult;
885
919
  detailResult.image_evidence = imageEvidence;
@@ -927,14 +961,14 @@ export async function runChatWorkflow({
927
961
  cardOnlyLlmResult = createMissingLlmConfigResult();
928
962
  } else {
929
963
  try {
930
- cardOnlyLlmResult = await callScreeningLlm({
964
+ cardOnlyLlmResult = await measureTiming(timings, "text_model_ms", () => callScreeningLlm({
931
965
  candidate: screeningCandidate,
932
966
  criteria,
933
967
  config: llmConfig,
934
968
  timeoutMs: llmTimeoutMs,
935
969
  maxImages: llmImageLimit,
936
970
  imageDetail: llmImageDetail
937
- });
971
+ }));
938
972
  } catch (error) {
939
973
  cardOnlyLlmResult = createFailedLlmResult(error);
940
974
  }
@@ -954,10 +988,10 @@ export async function runChatWorkflow({
954
988
  : screenCandidate(screeningCandidate, { criteria });
955
989
  let postAction = null;
956
990
  if (requestResumeForPassed && screening.passed) {
957
- postAction = await requestChatResumeForPassedCandidate(client, {
991
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
958
992
  greetingText,
959
993
  dryRun: dryRunRequestCv
960
- });
994
+ }));
961
995
  if (postAction?.requested) requestSatisfiedCount += 1;
962
996
  if (postAction?.skipped) requestSkippedCount += 1;
963
997
  if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
@@ -965,6 +999,7 @@ export async function runChatWorkflow({
965
999
  throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
966
1000
  }
967
1001
  }
1002
+ timings.total_ms = Date.now() - candidateStarted;
968
1003
  const compactResult = {
969
1004
  index,
970
1005
  candidate_key: candidateKey,
@@ -974,7 +1009,8 @@ export async function runChatWorkflow({
974
1009
  llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
975
1010
  screening: compactScreening(screening),
976
1011
  post_action: postAction,
977
- pre_action_state: preActionState
1012
+ pre_action_state: preActionState,
1013
+ timings
978
1014
  };
979
1015
  results.push(compactResult);
980
1016
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -1006,6 +1042,7 @@ export async function runChatWorkflow({
1006
1042
  last_candidate_key: candidateKey,
1007
1043
  last_score: screening.score
1008
1044
  });
1045
+ const checkpointStarted = Date.now();
1009
1046
  runControl.checkpoint({
1010
1047
  results,
1011
1048
  last_candidate: {
@@ -1020,9 +1057,13 @@ export async function runChatWorkflow({
1020
1057
  llm_screening: compactLlmResult(effectiveLlmResult)
1021
1058
  }
1022
1059
  });
1060
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1023
1061
 
1024
1062
  if (delayMs > 0) {
1063
+ const sleepStarted = Date.now();
1025
1064
  await runControl.sleep(delayMs);
1065
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1066
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1026
1067
  }
1027
1068
  }
1028
1069
 
@@ -1095,6 +1136,7 @@ export function createChatRunService({
1095
1136
  listWheelDeltaY = 850,
1096
1137
  listSettleMs = 1200,
1097
1138
  listFallbackPoint = null,
1139
+ imageOutputDir = "",
1098
1140
  name = "chat-domain-run"
1099
1141
  } = {}) {
1100
1142
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
@@ -1130,7 +1172,8 @@ export function createChatRunService({
1130
1172
  list_wheel_delta_y: listWheelDeltaY,
1131
1173
  list_settle_ms: listSettleMs,
1132
1174
  list_fallback_point: listFallbackPoint,
1133
- online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs
1175
+ online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
1176
+ image_output_dir: imageOutputDir || ""
1134
1177
  },
1135
1178
  progress: {
1136
1179
  card_count: 0,
@@ -1180,7 +1223,8 @@ export function createChatRunService({
1180
1223
  listStableSignatureLimit,
1181
1224
  listWheelDeltaY,
1182
1225
  listSettleMs,
1183
- listFallbackPoint
1226
+ listFallbackPoint,
1227
+ imageOutputDir
1184
1228
  }, runControl)
1185
1229
  });
1186
1230
  }
@@ -6,6 +6,10 @@ import {
6
6
  querySelectorAll,
7
7
  sleep
8
8
  } from "../../core/browser/index.js";
9
+ import {
10
+ mergeBossCandidateCardFields,
11
+ parseBossCandidateCardFieldsFromHtml
12
+ } from "../../core/boss-cards/index.js";
9
13
  import {
10
14
  htmlToText,
11
15
  normalizeCandidateFromHtml,
@@ -24,6 +28,16 @@ function normalizeRefreshButtonLabel(outerHTML = "") {
24
28
  return normalizeText(htmlToText(outerHTML)).replace(/\s+/g, "");
25
29
  }
26
30
 
31
+ export function parseRecommendCardFieldsFromHtml(html = "") {
32
+ return parseBossCandidateCardFieldsFromHtml(html);
33
+ }
34
+
35
+ function enrichRecommendCardCandidate(candidate, outerHTML = "") {
36
+ return mergeBossCandidateCardFields(candidate, outerHTML, {
37
+ metadataKey: "recommend_card_fields"
38
+ });
39
+ }
40
+
27
41
  function isRefreshButtonLabel(label = "") {
28
42
  const normalized = String(label || "").trim();
29
43
  if (!normalized || normalized.length > 80) return false;
@@ -91,7 +105,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
91
105
  getAttributesMap(client, cardNodeId),
92
106
  getOuterHTML(client, cardNodeId)
93
107
  ]);
94
- return normalizeCandidateFromHtml({
108
+ const candidate = normalizeCandidateFromHtml({
95
109
  domain: "recommend",
96
110
  source,
97
111
  html: outerHTML,
@@ -102,6 +116,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
102
116
  ...metadata
103
117
  }
104
118
  });
119
+ return enrichRecommendCardCandidate(candidate, outerHTML);
105
120
  }
106
121
 
107
122
  export async function readFirstRecommendCardCandidate(client, frameNodeId, options = {}) {
@@ -279,15 +279,25 @@ export async function openRecommendCardDetail(client, cardNodeId, {
279
279
  timeoutMs = 12000,
280
280
  scrollIntoView = true
281
281
  } = {}) {
282
+ const started = Date.now();
283
+ const clickStarted = Date.now();
282
284
  const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
285
+ const candidateClickMs = Date.now() - clickStarted;
286
+ const detailStarted = Date.now();
283
287
  const detailState = await waitForRecommendDetail(client, { timeoutMs });
288
+ const detailOpenMs = Date.now() - detailStarted;
284
289
  if (!detailState?.popup && !detailState?.resumeIframe) {
285
290
  throw new Error("Candidate detail did not open or no known detail selectors mounted");
286
291
  }
287
292
 
288
293
  return {
289
294
  card_box: cardBox,
290
- detail_state: detailState
295
+ detail_state: detailState,
296
+ timings: {
297
+ candidate_click_ms: candidateClickMs,
298
+ detail_open_ms: detailOpenMs,
299
+ open_total_ms: Date.now() - started
300
+ }
291
301
  };
292
302
  }
293
303
 
@@ -477,31 +487,40 @@ export async function extractRecommendDetailCandidate(client, {
477
487
  detailState,
478
488
  networkEvents = [],
479
489
  targetUrl = "",
480
- closeDetail = true
490
+ closeDetail = true,
491
+ networkParseRetryMs = 1800,
492
+ networkParseIntervalMs = 250
481
493
  } = {}) {
482
- await sleep(1000);
483
- const networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
484
494
  const detailHtml = await readRecommendDetailHtml(client, detailState);
485
495
  const detailText = [
486
496
  detailHtml.popupText,
487
497
  detailHtml.resumeText
488
498
  ].filter(Boolean).join("\n\n");
489
499
 
490
- const detailCandidateResult = buildScreeningCandidateFromDetail({
491
- cardCandidate,
492
- detailText,
493
- networkBodies,
494
- metadata: {
495
- target_url: targetUrl,
496
- card_node_id: cardNodeId,
497
- detail_popup_selector: detailState?.popup?.selector || null,
498
- detail_popup_root: detailState?.popup?.root || null,
499
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
500
- resume_iframe_root: detailState?.resumeIframe?.root || null,
501
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
502
- detail_html_errors: detailHtml.errors || []
503
- }
504
- });
500
+ const parseStarted = Date.now();
501
+ let networkBodies = [];
502
+ let detailCandidateResult = null;
503
+ do {
504
+ networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
505
+ detailCandidateResult = buildScreeningCandidateFromDetail({
506
+ cardCandidate,
507
+ detailText,
508
+ networkBodies,
509
+ metadata: {
510
+ target_url: targetUrl,
511
+ card_node_id: cardNodeId,
512
+ detail_popup_selector: detailState?.popup?.selector || null,
513
+ detail_popup_root: detailState?.popup?.root || null,
514
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
515
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
516
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
517
+ detail_html_errors: detailHtml.errors || []
518
+ }
519
+ });
520
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
521
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
522
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
523
+ } while (true);
505
524
 
506
525
  let closeResult = null;
507
526
  if (closeDetail) {
@@ -512,6 +531,8 @@ export async function extractRecommendDetailCandidate(client, {
512
531
  candidate: detailCandidateResult.candidate,
513
532
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
514
533
  network_bodies: networkBodies,
534
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
535
+ network_event_count: networkEvents.length,
515
536
  detail: {
516
537
  popup_text: detailHtml.popupText,
517
538
  resume_text: detailHtml.resumeText,
@@ -1,4 +1,9 @@
1
1
  import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import {
3
+ addTiming,
4
+ imageEvidenceFilePath,
5
+ measureTiming
6
+ } from "../../core/run/timing.js";
2
7
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
8
  import { sleep } from "../../core/browser/index.js";
4
9
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
@@ -377,7 +382,8 @@ export async function runRecommendWorkflow({
377
382
  llmConfig = null,
378
383
  llmTimeoutMs = 120000,
379
384
  llmImageLimit = 8,
380
- llmImageDetail = "high"
385
+ llmImageDetail = "high",
386
+ imageOutputDir = ""
381
387
  } = {}, runControl) {
382
388
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
383
389
  const normalizedFilter = normalizeFilter(filter);
@@ -520,12 +526,14 @@ export async function runRecommendWorkflow({
520
526
  });
521
527
 
522
528
  while (results.length < limit) {
529
+ const candidateStarted = Date.now();
530
+ const timings = {};
523
531
  await runControl.waitIfPaused();
524
532
  runControl.throwIfCanceled();
525
533
  runControl.setPhase("recommend:candidate");
526
534
  rootState = await ensureRecommendViewport(rootState, "candidate_loop");
527
535
 
528
- const nextCandidateResult = await getNextInfiniteListCandidate({
536
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
529
537
  client,
530
538
  state: listState,
531
539
  maxScrolls: listMaxScrolls,
@@ -552,7 +560,7 @@ export async function runRecommendWorkflow({
552
560
  visible_index: visibleIndex
553
561
  }
554
562
  })
555
- });
563
+ }));
556
564
  if (!nextCandidateResult.ok) {
557
565
  listEndReason = nextCandidateResult.reason || "list_exhausted";
558
566
  if (
@@ -644,11 +652,13 @@ export async function runRecommendWorkflow({
644
652
  targetUrl,
645
653
  maxAttempts: 2
646
654
  });
655
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
656
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
647
657
  cardNodeId = openedDetail.card_node_id || cardNodeId;
648
658
  cardCandidate = openedDetail.card_candidate || cardCandidate;
649
659
  screeningCandidate = cardCandidate;
650
660
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
651
- const networkWait = await waitForCvNetworkEvents(
661
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
652
662
  waitForRecommendDetailNetworkEvents,
653
663
  networkRecorder,
654
664
  {
@@ -657,15 +667,21 @@ export async function runRecommendWorkflow({
657
667
  requireLoaded: true,
658
668
  intervalMs: 120
659
669
  }
660
- );
670
+ ));
671
+ if (networkWait?.elapsed_ms != null) {
672
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
673
+ }
661
674
  detailResult = await extractRecommendDetailCandidate(client, {
662
675
  cardCandidate,
663
676
  cardNodeId,
664
677
  detailState: openedDetail.detail_state,
665
678
  networkEvents: networkRecorder.events,
666
679
  targetUrl,
667
- closeDetail: false
680
+ closeDetail: false,
681
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
682
+ networkParseIntervalMs: 250
668
683
  });
684
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
669
685
 
670
686
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
671
687
  let source = "network";
@@ -680,11 +696,25 @@ export async function runRecommendWorkflow({
680
696
  || openedDetail.detail_state?.resumeIframe?.node_id
681
697
  || null;
682
698
  if (captureNodeId) {
683
- imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
699
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
700
+ filePath: imageEvidenceFilePath({
701
+ imageOutputDir,
702
+ domain: "recommend",
703
+ runId: runControl?.runId,
704
+ index,
705
+ extension: "jpg"
706
+ }),
707
+ format: "jpeg",
708
+ quality: 72,
709
+ optimize: true,
710
+ resizeMaxWidth: 1100,
711
+ captureViewport: true,
684
712
  padding: 4,
685
713
  maxScreenshots: maxImagePages,
686
714
  wheelDeltaY: imageWheelDeltaY,
687
- settleMs: 1200,
715
+ settleMs: 350,
716
+ duplicateStopCount: 1,
717
+ skipDuplicateScreenshots: true,
688
718
  metadata: {
689
719
  domain: "recommend",
690
720
  capture_mode: "scroll_sequence",
@@ -692,7 +722,7 @@ export async function runRecommendWorkflow({
692
722
  run_candidate_index: index,
693
723
  candidate_key: candidateKey
694
724
  }
695
- });
725
+ }));
696
726
  source = "image";
697
727
  recordCvImageFallback(cvAcquisitionState, {
698
728
  parsedNetworkProfileCount,
@@ -730,7 +760,10 @@ export async function runRecommendWorkflow({
730
760
  llmResult = createMissingLlmConfigResult();
731
761
  } else {
732
762
  try {
733
- llmResult = await callScreeningLlm({
763
+ const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
764
+ ? "vision_model_ms"
765
+ : "text_model_ms";
766
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
734
767
  candidate: screeningCandidate,
735
768
  criteria,
736
769
  config: llmConfig,
@@ -738,7 +771,7 @@ export async function runRecommendWorkflow({
738
771
  imageEvidence: detailResult?.image_evidence || null,
739
772
  maxImages: llmImageLimit,
740
773
  imageDetail: llmImageDetail
741
- });
774
+ }));
742
775
  } catch (error) {
743
776
  llmResult = createFailedLlmScreeningResult(error);
744
777
  }
@@ -751,6 +784,7 @@ export async function runRecommendWorkflow({
751
784
  let actionDiscovery = null;
752
785
  let postActionResult = null;
753
786
  if (postActionEnabled && detailResult) {
787
+ const postActionStarted = Date.now();
754
788
  await runControl.waitIfPaused();
755
789
  runControl.throwIfCanceled();
756
790
  runControl.setPhase("recommend:post-action");
@@ -772,10 +806,12 @@ export async function runRecommendWorkflow({
772
806
  if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
773
807
  greetCount += 1;
774
808
  }
809
+ addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
775
810
  }
776
811
  if (detailResult && closeDetail) {
777
- detailResult.close_result = await closeRecommendDetail(client);
812
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
778
813
  }
814
+ timings.total_ms = Date.now() - candidateStarted;
779
815
  const compactResult = {
780
816
  index,
781
817
  candidate_key: candidateKey,
@@ -785,7 +821,8 @@ export async function runRecommendWorkflow({
785
821
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
786
822
  screening: compactScreening(screening),
787
823
  action_discovery: compactActionDiscovery(actionDiscovery),
788
- post_action: postActionResult
824
+ post_action: postActionResult,
825
+ timings
789
826
  };
790
827
  results.push(compactResult);
791
828
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -816,6 +853,7 @@ export async function runRecommendWorkflow({
816
853
  last_candidate_key: candidateKey,
817
854
  last_score: screening.score
818
855
  });
856
+ const checkpointStarted = Date.now();
819
857
  runControl.checkpoint({
820
858
  results,
821
859
  last_candidate: {
@@ -831,6 +869,7 @@ export async function runRecommendWorkflow({
831
869
  post_action: postActionResult
832
870
  }
833
871
  });
872
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
834
873
 
835
874
  if (postActionResult?.stop_run) {
836
875
  listEndReason = postActionResult.reason || "post_action_stop";
@@ -838,7 +877,10 @@ export async function runRecommendWorkflow({
838
877
  }
839
878
 
840
879
  if (delayMs > 0) {
880
+ const sleepStarted = Date.now();
841
881
  await runControl.sleep(delayMs);
882
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
883
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
842
884
  }
843
885
  }
844
886
 
@@ -912,6 +954,7 @@ export function createRecommendRunService({
912
954
  llmTimeoutMs = 120000,
913
955
  llmImageLimit = 8,
914
956
  llmImageDetail = "high",
957
+ imageOutputDir = "",
915
958
  name = "recommend-domain-run"
916
959
  } = {}) {
917
960
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
@@ -955,7 +998,8 @@ export function createRecommendRunService({
955
998
  llm_configured: Boolean(llmConfig),
956
999
  llm_timeout_ms: llmTimeoutMs,
957
1000
  llm_image_limit: llmImageLimit,
958
- llm_image_detail: llmImageDetail
1001
+ llm_image_detail: llmImageDetail,
1002
+ image_output_dir: imageOutputDir || ""
959
1003
  },
960
1004
  progress: {
961
1005
  card_count: 0,
@@ -1004,7 +1048,8 @@ export function createRecommendRunService({
1004
1048
  llmConfig,
1005
1049
  llmTimeoutMs,
1006
1050
  llmImageLimit,
1007
- llmImageDetail
1051
+ llmImageDetail,
1052
+ imageOutputDir
1008
1053
  }, runControl)
1009
1054
  });
1010
1055
  }