@reconcrap/boss-recommend-mcp 2.0.43 → 2.0.44

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.
@@ -38,6 +38,76 @@ function withPadding(rect, padding = 0) {
38
38
  };
39
39
  }
40
40
 
41
+ function normalizeRandom(random) {
42
+ return typeof random === "function" ? random : Math.random;
43
+ }
44
+
45
+ function randomBetween(random, min, max) {
46
+ const lower = Number(min) || 0;
47
+ const upper = Number(max) || lower;
48
+ if (upper <= lower) return lower;
49
+ return lower + normalizeRandom(random)() * (upper - lower);
50
+ }
51
+
52
+ function normalizeRatio(raw, fallback, { min = 0, max = 1 } = {}) {
53
+ const parsed = Number(raw);
54
+ const value = Number.isFinite(parsed) ? parsed : fallback;
55
+ return Math.min(max, Math.max(min, value));
56
+ }
57
+
58
+ function normalizeScrollDeltaJitter({
59
+ enabled = false,
60
+ minRatio = 0.65,
61
+ maxRatio = 0.9,
62
+ minOverlapRatio = 0.2,
63
+ preserveCoverage = true,
64
+ random = Math.random
65
+ } = {}) {
66
+ const safeMinRatio = normalizeRatio(minRatio, 0.65, { min: 0.1, max: 1 });
67
+ const safeMaxRatio = Math.max(safeMinRatio, normalizeRatio(maxRatio, 0.9, { min: safeMinRatio, max: 1 }));
68
+ return {
69
+ enabled: enabled === true,
70
+ min_ratio: safeMinRatio,
71
+ max_ratio: safeMaxRatio,
72
+ min_overlap_ratio: normalizeRatio(minOverlapRatio, 0.2, { min: 0, max: 0.8 }),
73
+ preserve_coverage: preserveCoverage !== false,
74
+ random: normalizeRandom(random)
75
+ };
76
+ }
77
+
78
+ function resolveCoverageSafeScrollDelta({
79
+ baseDelta,
80
+ clipHeight,
81
+ jitter
82
+ } = {}) {
83
+ const safeBase = Math.max(1, Number(baseDelta) || 650);
84
+ if (!jitter?.enabled) {
85
+ return {
86
+ deltaY: safeBase,
87
+ jittered: false,
88
+ base_delta_y: safeBase
89
+ };
90
+ }
91
+ const safeClipHeight = Math.max(1, Number(clipHeight) || 1);
92
+ const maxDeltaForOverlap = Math.max(1, Math.floor(safeClipHeight * (1 - jitter.min_overlap_ratio)));
93
+ const upper = Math.max(1, Math.min(Math.round(safeBase * jitter.max_ratio), maxDeltaForOverlap));
94
+ const lower = Math.min(upper, Math.max(1, Math.round(safeBase * jitter.min_ratio)));
95
+ const deltaY = Math.max(1, Math.round(randomBetween(jitter.random, lower, upper)));
96
+ return {
97
+ deltaY,
98
+ jittered: true,
99
+ base_delta_y: safeBase,
100
+ min_delta_y: lower,
101
+ max_delta_y: upper,
102
+ min_ratio: jitter.min_ratio,
103
+ max_ratio: jitter.max_ratio,
104
+ min_overlap_ratio: jitter.min_overlap_ratio,
105
+ clip_height: safeClipHeight,
106
+ max_delta_for_overlap: maxDeltaForOverlap,
107
+ preserve_coverage: jitter.preserve_coverage
108
+ };
109
+ }
110
+
41
111
  export async function captureNodeHtml(client, nodeId, {
42
112
  domain = "unknown",
43
113
  source = "dom",
@@ -709,6 +779,12 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
709
779
  scrollAnchorSelector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
710
780
  scrollAnchorMaxProbeNodes = 260,
711
781
  scrollAnchorMinGap = 180,
782
+ scrollDeltaJitterEnabled = false,
783
+ scrollDeltaJitterMinRatio = 0.65,
784
+ scrollDeltaJitterMaxRatio = 0.9,
785
+ scrollDeltaJitterMinOverlapRatio = 0.2,
786
+ scrollDeltaJitterPreserveCoverage = true,
787
+ scrollDeltaJitterRandom = Math.random,
712
788
  stopBoundarySelector = "",
713
789
  stopBoundaryTextPatterns = [],
714
790
  stopBoundaryMaxProbeNodes = 180,
@@ -721,10 +797,21 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
721
797
  const sequenceStarted = Date.now();
722
798
  const normalizedScrollMethod = normalizeScrollMethod(scrollMethod);
723
799
  const maxScreenshotCount = Math.max(1, Number(maxScreenshots) || 1);
800
+ const scrollDeltaJitter = normalizeScrollDeltaJitter({
801
+ enabled: scrollDeltaJitterEnabled,
802
+ minRatio: scrollDeltaJitterMinRatio,
803
+ maxRatio: scrollDeltaJitterMaxRatio,
804
+ minOverlapRatio: scrollDeltaJitterMinOverlapRatio,
805
+ preserveCoverage: scrollDeltaJitterPreserveCoverage,
806
+ random: scrollDeltaJitterRandom
807
+ });
808
+ const maxCaptureIterations = scrollDeltaJitter.enabled && scrollDeltaJitter.preserve_coverage
809
+ ? Math.max(maxScreenshotCount, Math.ceil(maxScreenshotCount / scrollDeltaJitter.min_ratio))
810
+ : maxScreenshotCount;
724
811
  const anchorPlan = normalizedScrollMethod !== "input"
725
812
  ? await collectDomScrollAnchors(client, nodeId, {
726
813
  selector: scrollAnchorSelector,
727
- maxScreenshots: maxScreenshotCount,
814
+ maxScreenshots: maxCaptureIterations,
728
815
  maxProbeNodes: scrollAnchorMaxProbeNodes,
729
816
  minAnchorGap: scrollAnchorMinGap,
730
817
  stepTimeoutMs
@@ -793,7 +880,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
793
880
  }
794
881
  }
795
882
 
796
- for (let index = 0; index < maxScreenshotCount; index += 1) {
883
+ for (let index = 0; index < maxCaptureIterations; index += 1) {
797
884
  assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `capture_page_${index + 1}`);
798
885
  captureCount += 1;
799
886
  const captureStarted = Date.now();
@@ -920,7 +1007,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
920
1007
  break;
921
1008
  }
922
1009
 
923
- if (index < maxScreenshotCount - 1) {
1010
+ if (index < maxCaptureIterations - 1) {
924
1011
  assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `scroll_after_page_${index + 1}`);
925
1012
  let scrolledByDomAnchor = false;
926
1013
  const nextAnchor = anchorPlan?.anchors?.[index + 1] || null;
@@ -956,6 +1043,11 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
956
1043
  if (!scrolledByDomAnchor && normalizedScrollMethod !== "dom-anchor") {
957
1044
  const x = box.center.x;
958
1045
  const y = box.center.y;
1046
+ const scrollDelta = resolveCoverageSafeScrollDelta({
1047
+ baseDelta: wheelDeltaY,
1048
+ clipHeight: effectiveClip.height,
1049
+ jitter: scrollDeltaJitter
1050
+ });
959
1051
  await withCaptureTimeout(client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" }), {
960
1052
  label: `scroll_mouse_move_${index + 1}`,
961
1053
  timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
@@ -965,7 +1057,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
965
1057
  x,
966
1058
  y,
967
1059
  deltaX: 0,
968
- deltaY: Math.max(1, Number(wheelDeltaY) || 650)
1060
+ deltaY: scrollDelta.deltaY
969
1061
  }), {
970
1062
  label: `scroll_wheel_${index + 1}`,
971
1063
  timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
@@ -973,7 +1065,10 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
973
1065
  currentScrollMetadata = {
974
1066
  before_capture: `wheel_down_${index + 1}`,
975
1067
  method: "Input.dispatchMouseEvent",
976
- fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input")
1068
+ fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input"),
1069
+ wheel_delta_y: scrollDelta.deltaY,
1070
+ wheel_delta_base_y: scrollDelta.base_delta_y,
1071
+ wheel_delta_jitter: scrollDelta.jittered ? scrollDelta : null
977
1072
  };
978
1073
  }
979
1074
  if (settleMs > 0) await sleep(settleMs);
@@ -1031,15 +1126,24 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
1031
1126
  step_timeout_ms: Math.max(0, Number(stepTimeoutMs) || 0),
1032
1127
  total_timeout_ms: Math.max(0, Number(totalTimeoutMs) || 0),
1033
1128
  scroll_method: normalizedScrollMethod,
1034
- scroll_anchor_selector: scrollAnchorSelector,
1035
- scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
1036
- scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0)
1037
- },
1038
- scroll_anchor_plan: anchorPlan,
1039
- stop_boundary_plan: stopBoundaryPlan,
1040
- stop_boundary_checks: stopBoundaryChecks,
1041
- stop_boundary_result: stopBoundaryResult,
1042
- file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
1129
+ requested_max_screenshots: maxScreenshotCount,
1130
+ effective_max_screenshots: maxCaptureIterations,
1131
+ scroll_anchor_selector: scrollAnchorSelector,
1132
+ scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
1133
+ scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0),
1134
+ scroll_delta_jitter: {
1135
+ enabled: scrollDeltaJitter.enabled,
1136
+ min_ratio: scrollDeltaJitter.min_ratio,
1137
+ max_ratio: scrollDeltaJitter.max_ratio,
1138
+ min_overlap_ratio: scrollDeltaJitter.min_overlap_ratio,
1139
+ preserve_coverage: scrollDeltaJitter.preserve_coverage
1140
+ }
1141
+ },
1142
+ scroll_anchor_plan: anchorPlan,
1143
+ stop_boundary_plan: stopBoundaryPlan,
1144
+ stop_boundary_checks: stopBoundaryChecks,
1145
+ stop_boundary_result: stopBoundaryResult,
1146
+ file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
1043
1147
  screenshots,
1044
1148
  metadata
1045
1149
  };
@@ -180,6 +180,83 @@ function normalizePoint(point) {
180
180
  return { x, y };
181
181
  }
182
182
 
183
+ function normalizeRandom(random) {
184
+ return typeof random === "function" ? random : Math.random;
185
+ }
186
+
187
+ function randomBetween(random, min, max) {
188
+ const lower = Number(min) || 0;
189
+ const upper = Number(max) || lower;
190
+ if (upper <= lower) return lower;
191
+ return lower + random() * (upper - lower);
192
+ }
193
+
194
+ function clampNumber(value, min, max) {
195
+ const number = Number(value);
196
+ if (!Number.isFinite(number)) return min;
197
+ return Math.min(max, Math.max(min, number));
198
+ }
199
+
200
+ export function resolveInfiniteListScrollTiming({
201
+ wheelDeltaY = 850,
202
+ settleMs = 1200,
203
+ listScrollJitterEnabled = false,
204
+ listScrollJitterMinRatio = 0.85,
205
+ listScrollJitterMaxRatio = 1.15,
206
+ listSettleJitterMinRatio = 0.75,
207
+ listSettleJitterMaxRatio = 1.35,
208
+ random = Math.random
209
+ } = {}) {
210
+ const baseDeltaY = Math.max(1, Number(wheelDeltaY) || 850);
211
+ const baseSettleMs = Math.max(0, Number(settleMs) || 0);
212
+ if (listScrollJitterEnabled !== true) {
213
+ return {
214
+ wheelDeltaY: baseDeltaY,
215
+ settleMs: baseSettleMs,
216
+ wheel_delta_jitter: {
217
+ enabled: false,
218
+ base_delta_y: baseDeltaY,
219
+ actual_delta_y: baseDeltaY
220
+ },
221
+ settle_jitter: {
222
+ enabled: false,
223
+ base_settle_ms: baseSettleMs,
224
+ actual_settle_ms: baseSettleMs
225
+ }
226
+ };
227
+ }
228
+ const nextRandom = normalizeRandom(random);
229
+ const minDeltaRatio = clampNumber(listScrollJitterMinRatio, 0.5, 1.5);
230
+ const maxDeltaRatio = clampNumber(listScrollJitterMaxRatio, minDeltaRatio, 1.5);
231
+ const minSettleRatio = clampNumber(listSettleJitterMinRatio, 0.4, 2);
232
+ const maxSettleRatio = clampNumber(listSettleJitterMaxRatio, minSettleRatio, 2);
233
+ const deltaRatio = randomBetween(nextRandom, minDeltaRatio, maxDeltaRatio);
234
+ const settleRatio = randomBetween(nextRandom, minSettleRatio, maxSettleRatio);
235
+ const actualDeltaY = Math.max(1, Math.round(baseDeltaY * deltaRatio));
236
+ const actualSettleMs = Math.max(0, Math.round(baseSettleMs * settleRatio));
237
+ return {
238
+ wheelDeltaY: actualDeltaY,
239
+ settleMs: actualSettleMs,
240
+ wheel_delta_jitter: {
241
+ enabled: true,
242
+ preserve_coverage: true,
243
+ base_delta_y: baseDeltaY,
244
+ actual_delta_y: actualDeltaY,
245
+ ratio: deltaRatio,
246
+ min_ratio: minDeltaRatio,
247
+ max_ratio: maxDeltaRatio
248
+ },
249
+ settle_jitter: {
250
+ enabled: true,
251
+ base_settle_ms: baseSettleMs,
252
+ actual_settle_ms: actualSettleMs,
253
+ ratio: settleRatio,
254
+ min_ratio: minSettleRatio,
255
+ max_ratio: maxSettleRatio
256
+ }
257
+ };
258
+ }
259
+
183
260
  function resolveViewportPoint(viewportPoint, viewport) {
184
261
  if (!viewportPoint) return null;
185
262
  if (viewport && ("xRatio" in viewportPoint || "yRatio" in viewportPoint)) {
@@ -891,7 +968,13 @@ export function firstUnseenInfiniteListItem(state, items = []) {
891
968
  export async function scrollInfiniteListByVisibleItems(client, items = [], {
892
969
  wheelDeltaY = 850,
893
970
  settleMs = 1200,
894
- fallbackPoint = null
971
+ fallbackPoint = null,
972
+ listScrollJitterEnabled = false,
973
+ listScrollJitterMinRatio = 0.85,
974
+ listScrollJitterMaxRatio = 1.15,
975
+ listSettleJitterMinRatio = 0.75,
976
+ listSettleJitterMaxRatio = 1.35,
977
+ random = Math.random
895
978
  } = {}) {
896
979
  const candidates = items.filter((item) => item?.node_id);
897
980
  if (!candidates.length) {
@@ -902,7 +985,18 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
902
985
  }
903
986
 
904
987
  const errors = [];
905
- const wheelDelta = Math.max(1, Number(wheelDeltaY) || 850);
988
+ const scrollTiming = resolveInfiniteListScrollTiming({
989
+ wheelDeltaY,
990
+ settleMs,
991
+ listScrollJitterEnabled,
992
+ listScrollJitterMinRatio,
993
+ listScrollJitterMaxRatio,
994
+ listSettleJitterMinRatio,
995
+ listSettleJitterMaxRatio,
996
+ random
997
+ });
998
+ const wheelDelta = scrollTiming.wheelDeltaY;
999
+ const actualSettleMs = scrollTiming.settleMs;
906
1000
  async function synthesizeGesture(x, y) {
907
1001
  if (typeof client?.Input?.synthesizeScrollGesture !== "function") return null;
908
1002
  try {
@@ -941,15 +1035,19 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
941
1035
  deltaY: wheelDelta
942
1036
  });
943
1037
  const gesture = await synthesizeGesture(x, y);
944
- if (settleMs > 0) await sleep(settleMs);
1038
+ if (actualSettleMs > 0) await sleep(actualSettleMs);
945
1039
  return {
946
1040
  ok: true,
947
1041
  anchor_key: anchor.key,
948
1042
  anchor_node_id: anchor.node_id,
949
1043
  point: { x, y },
950
1044
  wheel_delta_y: wheelDelta,
1045
+ base_wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
1046
+ wheel_delta_jitter: scrollTiming.wheel_delta_jitter,
951
1047
  gesture,
952
- settle_ms: settleMs,
1048
+ settle_ms: actualSettleMs,
1049
+ base_settle_ms: Math.max(0, Number(settleMs) || 0),
1050
+ settle_jitter: scrollTiming.settle_jitter,
953
1051
  skipped_stale_anchor_count: errors.length
954
1052
  };
955
1053
  } catch (error) {
@@ -994,7 +1092,7 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
994
1092
  deltaY: wheelDelta
995
1093
  });
996
1094
  const gesture = await synthesizeGesture(x, y);
997
- if (settleMs > 0) await sleep(settleMs);
1095
+ if (actualSettleMs > 0) await sleep(actualSettleMs);
998
1096
  return {
999
1097
  ok: true,
1000
1098
  mode: "fallback_point",
@@ -1010,8 +1108,12 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
1010
1108
  assist,
1011
1109
  point: { x, y },
1012
1110
  wheel_delta_y: wheelDelta,
1111
+ base_wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
1112
+ wheel_delta_jitter: scrollTiming.wheel_delta_jitter,
1013
1113
  gesture,
1014
- settle_ms: settleMs,
1114
+ settle_ms: actualSettleMs,
1115
+ base_settle_ms: Math.max(0, Number(settleMs) || 0),
1116
+ settle_jitter: scrollTiming.settle_jitter,
1015
1117
  skipped_stale_anchor_count: errors.length,
1016
1118
  stale_anchor_errors: errors
1017
1119
  };
@@ -1037,7 +1139,13 @@ export async function getNextInfiniteListCandidate({
1037
1139
  minScrollsBeforeEnd = 3,
1038
1140
  wheelDeltaY = 850,
1039
1141
  settleMs = 1200,
1040
- fallbackPoint = null
1142
+ fallbackPoint = null,
1143
+ listScrollJitterEnabled = false,
1144
+ listScrollJitterMinRatio = 0.85,
1145
+ listScrollJitterMaxRatio = 1.15,
1146
+ listSettleJitterMinRatio = 0.75,
1147
+ listSettleJitterMaxRatio = 1.35,
1148
+ random = Math.random
1041
1149
  } = {}) {
1042
1150
  if (!client) throw new Error("getNextInfiniteListCandidate requires client");
1043
1151
  if (!state) throw new Error("getNextInfiniteListCandidate requires state");
@@ -1173,7 +1281,13 @@ export async function getNextInfiniteListCandidate({
1173
1281
  const scrollResult = await scrollInfiniteListByVisibleItems(client, items, {
1174
1282
  wheelDeltaY,
1175
1283
  settleMs,
1176
- fallbackPoint
1284
+ fallbackPoint,
1285
+ listScrollJitterEnabled,
1286
+ listScrollJitterMinRatio,
1287
+ listScrollJitterMaxRatio,
1288
+ listSettleJitterMinRatio,
1289
+ listSettleJitterMaxRatio,
1290
+ random
1177
1291
  });
1178
1292
  state.scroll_count += scrollResult.ok ? 1 : 0;
1179
1293
  attempts[attempts.length - 1].scroll_result = scrollResult;
@@ -2,7 +2,11 @@ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
2
2
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
3
3
  import {
4
4
  clickPoint,
5
+ configureHumanInteraction,
6
+ createHumanRestController,
5
7
  getNodeBox,
8
+ humanDelay,
9
+ normalizeHumanBehaviorOptions,
6
10
  scrollNodeIntoView,
7
11
  sleep
8
12
  } from "../../core/browser/index.js";
@@ -685,9 +689,27 @@ export async function runChatWorkflow({
685
689
  listWheelDeltaY = 850,
686
690
  listSettleMs = 2200,
687
691
  listFallbackPoint = null,
688
- imageOutputDir = ""
692
+ imageOutputDir = "",
693
+ humanRestEnabled = false,
694
+ humanBehavior = null
689
695
  } = {}, runControl) {
690
696
  if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
697
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
698
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
699
+ });
700
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
701
+ configureHumanInteraction(client, {
702
+ enabled: effectiveHumanBehavior.enabled,
703
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
704
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
705
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
706
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
707
+ });
708
+ const humanRestController = createHumanRestController({
709
+ enabled: effectiveHumanRestEnabled,
710
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
711
+ batchRestEnabled: effectiveHumanBehavior.batchRest
712
+ });
691
713
  const normalizedDetailSource = normalizeDetailSource(detailSource);
692
714
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
693
715
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
@@ -732,6 +754,33 @@ export async function runChatWorkflow({
732
754
  let requestSatisfiedCount = 0;
733
755
  let requestSkippedCount = 0;
734
756
  let contextSetup = {};
757
+ let lastHumanEvent = null;
758
+
759
+ function recordHumanEvent(event = null) {
760
+ if (!event) return lastHumanEvent;
761
+ lastHumanEvent = {
762
+ at: new Date().toISOString(),
763
+ ...event
764
+ };
765
+ return lastHumanEvent;
766
+ }
767
+
768
+ async function maybeHumanActionCooldown(phase, timings = {}) {
769
+ if (!effectiveHumanBehavior.actionCooldown) return null;
770
+ const pauseMs = humanDelay(280, 90, {
771
+ minMs: 80,
772
+ maxMs: 720
773
+ });
774
+ if (pauseMs > 0) {
775
+ await runControl.sleep(pauseMs);
776
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
777
+ }
778
+ return recordHumanEvent({
779
+ kind: "action_cooldown",
780
+ phase,
781
+ pause_ms: pauseMs
782
+ });
783
+ }
735
784
 
736
785
  runControl.setPhase("chat:cleanup");
737
786
  let initialTopLevelState = await getChatTopLevelState(client);
@@ -870,7 +919,13 @@ export async function runChatWorkflow({
870
919
  scroll_count: compactInfiniteListState(listState).scroll_count,
871
920
  list_end_reason: listEndReason,
872
921
  viewport_checks: viewportGuard.getStats().checks,
873
- viewport_recoveries: viewportGuard.getStats().recoveries
922
+ viewport_recoveries: viewportGuard.getStats().recoveries,
923
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
924
+ human_behavior_profile: effectiveHumanBehavior.profile,
925
+ human_rest_enabled: effectiveHumanRestEnabled,
926
+ human_rest_count: humanRestController.getState().rest_count,
927
+ human_rest_ms: humanRestController.getState().total_rest_ms,
928
+ last_human_event: lastHumanEvent
874
929
  });
875
930
  runControl.setPhase("chat:done");
876
931
  return {
@@ -888,6 +943,9 @@ export async function runChatWorkflow({
888
943
  stats: viewportGuard.getStats(),
889
944
  events: viewportGuard.getEvents()
890
945
  },
946
+ human_behavior: effectiveHumanBehavior,
947
+ human_rest: humanRestController.getState(),
948
+ last_human_event: lastHumanEvent,
891
949
  list_end_reason: listEndReason,
892
950
  target_pass_count: passTarget,
893
951
  process_until_list_end: Boolean(processUntilListEnd),
@@ -993,6 +1051,7 @@ export async function runChatWorkflow({
993
1051
  stableSignatureLimit: listStableSignatureLimit,
994
1052
  wheelDeltaY: listWheelDeltaY,
995
1053
  settleMs: listSettleMs,
1054
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
996
1055
  fallbackPoint: listFallbackResolver,
997
1056
  findNodeIds: async () => {
998
1057
  const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
@@ -1074,6 +1133,7 @@ export async function runChatWorkflow({
1074
1133
 
1075
1134
  detailStep = "select_candidate";
1076
1135
  networkRecorder.clear();
1136
+ await maybeHumanActionCooldown("before_detail_open", timings);
1077
1137
  const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1078
1138
  cardNodeId,
1079
1139
  candidate: cardCandidate,
@@ -1178,6 +1238,7 @@ export async function runChatWorkflow({
1178
1238
  if (!detailResult) {
1179
1239
  detailStep = "open_online_resume";
1180
1240
  networkRecorder.clear();
1241
+ await maybeHumanActionCooldown("before_resume_open", timings);
1181
1242
  const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
1182
1243
  timeoutMs: readyTimeoutMs
1183
1244
  }));
@@ -1332,6 +1393,7 @@ export async function runChatWorkflow({
1332
1393
  wheelDeltaY: imageWheelDeltaY,
1333
1394
  settleMs: 350,
1334
1395
  scrollMethod: "dom-anchor-fallback-input",
1396
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1335
1397
  stepTimeoutMs: 45000,
1336
1398
  totalTimeoutMs: 90000,
1337
1399
  duplicateStopCount: 1,
@@ -1479,6 +1541,7 @@ export async function runChatWorkflow({
1479
1541
  full_cv_evidence: fullCvEvidence
1480
1542
  });
1481
1543
  closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
1544
+ await maybeHumanActionCooldown("after_detail_close", timings);
1482
1545
  if (!closeResult?.closed) {
1483
1546
  closeRecovery = await recoverAndReapplyChatContext(
1484
1547
  "resume_modal_close_failed:close_resume_modal",
@@ -1575,6 +1638,7 @@ export async function runChatWorkflow({
1575
1638
  : screenCandidate(screeningCandidate, { criteria });
1576
1639
  let postAction = null;
1577
1640
  if (requestResumeForPassed && screening.passed) {
1641
+ await maybeHumanActionCooldown("before_post_action", timings);
1578
1642
  postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1579
1643
  greetingText,
1580
1644
  dryRun: dryRunRequestCv
@@ -1627,6 +1691,12 @@ export async function runChatWorkflow({
1627
1691
  list_end_reason: listEndReason || null,
1628
1692
  viewport_checks: viewportGuard.getStats().checks,
1629
1693
  viewport_recoveries: viewportGuard.getStats().recoveries,
1694
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1695
+ human_behavior_profile: effectiveHumanBehavior.profile,
1696
+ human_rest_enabled: effectiveHumanRestEnabled,
1697
+ human_rest_count: humanRestController.getState().rest_count,
1698
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1699
+ last_human_event: lastHumanEvent,
1630
1700
  last_candidate_id: screeningCandidate.id || null,
1631
1701
  last_candidate_key: candidateKey,
1632
1702
  last_score: screening.score
@@ -1649,6 +1719,31 @@ export async function runChatWorkflow({
1649
1719
  });
1650
1720
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1651
1721
 
1722
+ if (effectiveHumanRestEnabled) {
1723
+ const restStarted = Date.now();
1724
+ const restResult = await humanRestController.takeBreakIfNeeded({
1725
+ sleepFn: (ms) => runControl.sleep(ms)
1726
+ });
1727
+ const restElapsed = Date.now() - restStarted;
1728
+ if (restResult.rested) {
1729
+ recordHumanEvent({
1730
+ kind: "rest",
1731
+ pause_ms: restResult.pause_ms || restElapsed,
1732
+ events: restResult.events || []
1733
+ });
1734
+ compactResult.human_rest = restResult;
1735
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1736
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1737
+ runControl.updateProgress({
1738
+ human_rest_enabled: effectiveHumanRestEnabled,
1739
+ human_rest_count: humanRestController.getState().rest_count,
1740
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1741
+ human_rest_last: restResult,
1742
+ last_human_event: lastHumanEvent
1743
+ });
1744
+ }
1745
+ }
1746
+
1652
1747
  if (delayMs > 0) {
1653
1748
  const sleepStarted = Date.now();
1654
1749
  await runControl.sleep(delayMs);
@@ -1669,6 +1764,9 @@ export async function runChatWorkflow({
1669
1764
  stats: viewportGuard.getStats(),
1670
1765
  events: viewportGuard.getEvents()
1671
1766
  },
1767
+ human_behavior: effectiveHumanBehavior,
1768
+ human_rest: humanRestController.getState(),
1769
+ last_human_event: lastHumanEvent,
1672
1770
  list_end_reason: listEndReason || null,
1673
1771
  target_pass_count: passTarget,
1674
1772
  process_until_list_end: Boolean(processUntilListEnd),
@@ -1730,6 +1828,8 @@ export function createChatRunService({
1730
1828
  listSettleMs = 2200,
1731
1829
  listFallbackPoint = null,
1732
1830
  imageOutputDir = "",
1831
+ humanRestEnabled = false,
1832
+ humanBehavior = null,
1733
1833
  name = "chat-domain-run"
1734
1834
  } = {}) {
1735
1835
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
@@ -1737,6 +1837,10 @@ export function createChatRunService({
1737
1837
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1738
1838
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1739
1839
  const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
1840
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1841
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1842
+ });
1843
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1740
1844
  return manager.startRun({
1741
1845
  name,
1742
1846
  context: {
@@ -1769,7 +1873,11 @@ export function createChatRunService({
1769
1873
  list_settle_ms: listSettleMs,
1770
1874
  list_fallback_point: listFallbackPoint,
1771
1875
  online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
1772
- image_output_dir: imageOutputDir || ""
1876
+ image_output_dir: imageOutputDir || "",
1877
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1878
+ human_behavior_profile: effectiveHumanBehavior.profile,
1879
+ human_behavior: effectiveHumanBehavior,
1880
+ human_rest_enabled: effectiveHumanRestEnabled
1773
1881
  },
1774
1882
  progress: {
1775
1883
  card_count: 0,
@@ -1784,7 +1892,13 @@ export function createChatRunService({
1784
1892
  skipped: 0,
1785
1893
  requested: 0,
1786
1894
  request_satisfied: 0,
1787
- request_skipped: 0
1895
+ request_skipped: 0,
1896
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1897
+ human_behavior_profile: effectiveHumanBehavior.profile,
1898
+ human_rest_enabled: effectiveHumanRestEnabled,
1899
+ human_rest_count: 0,
1900
+ human_rest_ms: 0,
1901
+ last_human_event: null
1788
1902
  },
1789
1903
  checkpoint: {},
1790
1904
  task: (runControl) => workflow({
@@ -1821,7 +1935,9 @@ export function createChatRunService({
1821
1935
  listWheelDeltaY,
1822
1936
  listSettleMs,
1823
1937
  listFallbackPoint,
1824
- imageOutputDir
1938
+ imageOutputDir,
1939
+ humanRestEnabled: effectiveHumanRestEnabled,
1940
+ humanBehavior: effectiveHumanBehavior
1825
1941
  }, runControl)
1826
1942
  });
1827
1943
  }