@reconcrap/boss-recommend-mcp 2.0.43 → 2.0.45

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.
@@ -8,7 +8,13 @@ import {
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
10
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
- import { sleep } from "../../core/browser/index.js";
11
+ import {
12
+ configureHumanInteraction,
13
+ createHumanRestController,
14
+ humanDelay,
15
+ normalizeHumanBehaviorOptions,
16
+ sleep
17
+ } from "../../core/browser/index.js";
12
18
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
13
19
  import {
14
20
  compactCvAcquisitionState,
@@ -568,13 +574,31 @@ export async function runRecommendWorkflow({
568
574
  actionAfterClickDelayMs = 900,
569
575
  screeningMode = "llm",
570
576
  llmConfig = null,
571
- llmTimeoutMs = 120000,
572
- llmImageLimit = 8,
573
- llmImageDetail = "high",
574
- imageOutputDir = ""
575
- } = {}, runControl) {
576
- if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
577
- const normalizedFilter = normalizeFilter(filter);
577
+ llmTimeoutMs = 120000,
578
+ llmImageLimit = 8,
579
+ llmImageDetail = "high",
580
+ imageOutputDir = "",
581
+ humanRestEnabled = false,
582
+ humanBehavior = null
583
+ } = {}, runControl) {
584
+ if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
585
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
586
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
587
+ });
588
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
589
+ configureHumanInteraction(client, {
590
+ enabled: effectiveHumanBehavior.enabled,
591
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
592
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
593
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
594
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
595
+ });
596
+ const humanRestController = createHumanRestController({
597
+ enabled: effectiveHumanRestEnabled,
598
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
599
+ batchRestEnabled: effectiveHumanBehavior.batchRest
600
+ });
601
+ const normalizedFilter = normalizeFilter(filter);
578
602
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
579
603
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
580
604
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
@@ -611,26 +635,54 @@ export async function runRecommendWorkflow({
611
635
  let greetCount = 0;
612
636
  const candidateRecoveryCounts = new Map();
613
637
  let jobSelection = null;
614
- let pageScopeSelection = null;
615
- let filterResult = null;
616
- let cardNodeIds = [];
617
- let listEndReason = "";
618
- const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
638
+ let pageScopeSelection = null;
639
+ let filterResult = null;
640
+ let cardNodeIds = [];
641
+ let listEndReason = "";
642
+ let lastHumanEvent = null;
643
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
619
644
  rootNodeId: rootState?.iframe?.documentNodeId,
620
645
  containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
621
646
  itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
622
647
  itemSelectors: [RECOMMEND_CARD_SELECTOR],
623
648
  viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
624
649
  validateViewportPoint: true
625
- }));
626
-
627
- function updateRecommendProgress(extra = {}) {
628
- const counts = countRecommendResultStatuses(results, { greetCount });
629
- const listSnapshot = compactInfiniteListState(listState);
630
- runControl.updateProgress({
631
- card_count: cardNodeIds.length,
632
- target_count: targetPassCount,
633
- target_count_semantics: "passed_candidates",
650
+ }));
651
+
652
+ function recordHumanEvent(event = null) {
653
+ if (!event) return lastHumanEvent;
654
+ lastHumanEvent = {
655
+ at: new Date().toISOString(),
656
+ ...event
657
+ };
658
+ return lastHumanEvent;
659
+ }
660
+
661
+ async function maybeHumanActionCooldown(phase, timings = {}) {
662
+ if (!effectiveHumanBehavior.actionCooldown) return null;
663
+ const pauseMs = humanDelay(280, 90, {
664
+ minMs: 80,
665
+ maxMs: 720
666
+ });
667
+ if (pauseMs > 0) {
668
+ await runControl.sleep(pauseMs);
669
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
670
+ }
671
+ return recordHumanEvent({
672
+ kind: "action_cooldown",
673
+ phase,
674
+ pause_ms: pauseMs
675
+ });
676
+ }
677
+
678
+ function updateRecommendProgress(extra = {}) {
679
+ const counts = countRecommendResultStatuses(results, { greetCount });
680
+ const listSnapshot = compactInfiniteListState(listState);
681
+ const humanRestState = humanRestController.getState();
682
+ runControl.updateProgress({
683
+ card_count: cardNodeIds.length,
684
+ target_count: targetPassCount,
685
+ target_count_semantics: "passed_candidates",
634
686
  ...counts,
635
687
  screening_mode: normalizedScreeningMode,
636
688
  unique_seen: listSnapshot.seen_count,
@@ -638,12 +690,18 @@ export async function runRecommendWorkflow({
638
690
  refresh_rounds: refreshRounds,
639
691
  refresh_attempts: refreshAttempts.length,
640
692
  context_recoveries: contextRecoveryAttempts,
641
- list_end_reason: listEndReason || null,
642
- viewport_checks: viewportGuard.getStats().checks,
643
- viewport_recoveries: viewportGuard.getStats().recoveries,
644
- ...extra
645
- });
646
- }
693
+ list_end_reason: listEndReason || null,
694
+ viewport_checks: viewportGuard.getStats().checks,
695
+ viewport_recoveries: viewportGuard.getStats().recoveries,
696
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
697
+ human_behavior_profile: effectiveHumanBehavior.profile,
698
+ human_rest_enabled: effectiveHumanRestEnabled,
699
+ human_rest_count: humanRestState.rest_count,
700
+ human_rest_ms: humanRestState.total_rest_ms,
701
+ last_human_event: lastHumanEvent,
702
+ ...extra
703
+ });
704
+ }
647
705
 
648
706
  function checkpointInProgressCandidate({
649
707
  index = results.length,
@@ -835,10 +893,11 @@ export async function runRecommendWorkflow({
835
893
  client,
836
894
  state: listState,
837
895
  maxScrolls: listMaxScrolls,
838
- stableSignatureLimit: listStableSignatureLimit,
839
- wheelDeltaY: listWheelDeltaY,
840
- settleMs: listSettleMs,
841
- fallbackPoint: listFallbackResolver,
896
+ stableSignatureLimit: listStableSignatureLimit,
897
+ wheelDeltaY: listWheelDeltaY,
898
+ settleMs: listSettleMs,
899
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
900
+ fallbackPoint: listFallbackResolver,
842
901
  findNodeIds: async () => {
843
902
  let currentRootState = await getRecommendRoots(client);
844
903
  currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
@@ -940,10 +999,11 @@ export async function runRecommendWorkflow({
940
999
  runControl.setPhase("recommend:detail");
941
1000
  detailStep = "ensure_viewport";
942
1001
  rootState = await ensureRecommendViewport(rootState, "detail");
943
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
944
- detailStep = "open_detail";
945
- networkRecorder.clear();
946
- const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1002
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1003
+ detailStep = "open_detail";
1004
+ networkRecorder.clear();
1005
+ await maybeHumanActionCooldown("before_detail_open", timings);
1006
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
947
1007
  cardNodeId,
948
1008
  candidateKey,
949
1009
  cardCandidate,
@@ -1022,11 +1082,12 @@ export async function runRecommendWorkflow({
1022
1082
  resizeMaxWidth: 1100,
1023
1083
  captureViewport: false,
1024
1084
  padding: 0,
1025
- maxScreenshots: maxImagePages,
1026
- wheelDeltaY: imageWheelDeltaY,
1027
- settleMs: 350,
1028
- scrollMethod: "dom-anchor-fallback-input",
1029
- stepTimeoutMs: 45000,
1085
+ maxScreenshots: maxImagePages,
1086
+ wheelDeltaY: imageWheelDeltaY,
1087
+ settleMs: 350,
1088
+ scrollMethod: "dom-anchor-fallback-input",
1089
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1090
+ stepTimeoutMs: 45000,
1030
1091
  totalTimeoutMs: 90000,
1031
1092
  duplicateStopCount: 1,
1032
1093
  skipDuplicateScreenshots: true,
@@ -1160,9 +1221,10 @@ export async function runRecommendWorkflow({
1160
1221
  if (postActionEnabled && detailResult) {
1161
1222
  const postActionStarted = Date.now();
1162
1223
  await runControl.waitIfPaused();
1163
- runControl.throwIfCanceled();
1164
- runControl.setPhase("recommend:post-action");
1165
- actionDiscovery = await waitForRecommendDetailActionControls(client, {
1224
+ runControl.throwIfCanceled();
1225
+ runControl.setPhase("recommend:post-action");
1226
+ await maybeHumanActionCooldown("before_post_action", timings);
1227
+ actionDiscovery = await waitForRecommendDetailActionControls(client, {
1166
1228
  timeoutMs: actionTimeoutMs,
1167
1229
  intervalMs: actionIntervalMs,
1168
1230
  requireAny: true
@@ -1181,10 +1243,11 @@ export async function runRecommendWorkflow({
1181
1243
  greetCount += 1;
1182
1244
  }
1183
1245
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1184
- }
1185
- if (detailResult && closeDetail) {
1186
- detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1187
- if (!detailResult.close_result?.closed) {
1246
+ }
1247
+ if (detailResult && closeDetail) {
1248
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1249
+ await maybeHumanActionCooldown("after_detail_close", timings);
1250
+ if (!detailResult.close_result?.closed) {
1188
1251
  const closeError = createRecommendCloseFailureError(detailResult.close_result);
1189
1252
  const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
1190
1253
  forceRecentNotView: true
@@ -1253,16 +1316,37 @@ export async function runRecommendWorkflow({
1253
1316
  });
1254
1317
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1255
1318
 
1256
- if (postActionResult?.stop_run) {
1257
- listEndReason = postActionResult.reason || "post_action_stop";
1258
- break;
1259
- }
1260
-
1261
- if (delayMs > 0) {
1262
- const sleepStarted = Date.now();
1263
- await runControl.sleep(delayMs);
1264
- addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1265
- compactResult.timings.total_ms = Date.now() - candidateStarted;
1319
+ if (postActionResult?.stop_run) {
1320
+ listEndReason = postActionResult.reason || "post_action_stop";
1321
+ break;
1322
+ }
1323
+
1324
+ if (effectiveHumanRestEnabled) {
1325
+ const restStarted = Date.now();
1326
+ const restResult = await humanRestController.takeBreakIfNeeded({
1327
+ sleepFn: (ms) => runControl.sleep(ms)
1328
+ });
1329
+ const restElapsed = Date.now() - restStarted;
1330
+ if (restResult.rested) {
1331
+ recordHumanEvent({
1332
+ kind: "rest",
1333
+ pause_ms: restResult.pause_ms || restElapsed,
1334
+ events: restResult.events || []
1335
+ });
1336
+ compactResult.human_rest = restResult;
1337
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1338
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1339
+ updateRecommendProgress({
1340
+ human_rest_last: restResult
1341
+ });
1342
+ }
1343
+ }
1344
+
1345
+ if (delayMs > 0) {
1346
+ const sleepStarted = Date.now();
1347
+ await runControl.sleep(delayMs);
1348
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1349
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1266
1350
  }
1267
1351
  }
1268
1352
 
@@ -1275,11 +1359,14 @@ export async function runRecommendWorkflow({
1275
1359
  filter: compactFilterResult(filterResult),
1276
1360
  card_count: cardNodeIds.length,
1277
1361
  candidate_list: compactInfiniteListState(listState),
1278
- viewport_health: {
1279
- stats: viewportGuard.getStats(),
1280
- events: viewportGuard.getEvents()
1281
- },
1282
- list_end_reason: listEndReason || null,
1362
+ viewport_health: {
1363
+ stats: viewportGuard.getStats(),
1364
+ events: viewportGuard.getEvents()
1365
+ },
1366
+ human_behavior: effectiveHumanBehavior,
1367
+ human_rest: humanRestController.getState(),
1368
+ last_human_event: lastHumanEvent,
1369
+ list_end_reason: listEndReason || null,
1283
1370
  refresh_rounds: refreshRounds,
1284
1371
  refresh_attempts: refreshAttempts,
1285
1372
  context_recoveries: contextRecoveryAttempts,
@@ -1330,20 +1417,26 @@ export function createRecommendRunService({
1330
1417
  actionIntervalMs = 500,
1331
1418
  actionAfterClickDelayMs = 900,
1332
1419
  screeningMode = "llm",
1333
- llmConfig = null,
1334
- llmTimeoutMs = 120000,
1335
- llmImageLimit = 8,
1336
- llmImageDetail = "high",
1337
- imageOutputDir = "",
1338
- name = "recommend-domain-run"
1339
- } = {}) {
1340
- if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1341
- const normalizedFilter = normalizeFilter(filter);
1342
- const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1343
- const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1344
- const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1345
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1346
- const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1420
+ llmConfig = null,
1421
+ llmTimeoutMs = 120000,
1422
+ llmImageLimit = 8,
1423
+ llmImageDetail = "high",
1424
+ imageOutputDir = "",
1425
+ humanRestEnabled = false,
1426
+ humanBehavior = null,
1427
+ name = "recommend-domain-run"
1428
+ } = {}) {
1429
+ if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1430
+ const normalizedFilter = normalizeFilter(filter);
1431
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1432
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1433
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1434
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1435
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1436
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1437
+ });
1438
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1439
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1347
1440
  const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1348
1441
  return manager.startRun({
1349
1442
  runId,
@@ -1379,12 +1472,16 @@ export function createRecommendRunService({
1379
1472
  action_timeout_ms: actionTimeoutMs,
1380
1473
  screening_mode: normalizedScreeningMode,
1381
1474
  llm_configured: Boolean(llmConfig),
1382
- llm_timeout_ms: llmTimeoutMs,
1383
- llm_image_limit: llmImageLimit,
1384
- llm_image_detail: llmImageDetail,
1385
- image_output_dir: imageOutputDir || ""
1386
- },
1387
- progress: {
1475
+ llm_timeout_ms: llmTimeoutMs,
1476
+ llm_image_limit: llmImageLimit,
1477
+ llm_image_detail: llmImageDetail,
1478
+ image_output_dir: imageOutputDir || "",
1479
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1480
+ human_behavior_profile: effectiveHumanBehavior.profile,
1481
+ human_behavior: effectiveHumanBehavior,
1482
+ human_rest_enabled: effectiveHumanRestEnabled
1483
+ },
1484
+ progress: {
1388
1485
  card_count: 0,
1389
1486
  target_count: candidateLimit,
1390
1487
  target_count_semantics: "passed_candidates",
@@ -1395,11 +1492,17 @@ export function createRecommendRunService({
1395
1492
  passed: 0,
1396
1493
  greet_count: 0,
1397
1494
  post_action_clicked: 0,
1398
- image_capture_failed: 0,
1399
- detail_open_failed: 0,
1400
- transient_recovered: 0,
1401
- context_recoveries: 0
1402
- },
1495
+ image_capture_failed: 0,
1496
+ detail_open_failed: 0,
1497
+ transient_recovered: 0,
1498
+ context_recoveries: 0,
1499
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1500
+ human_behavior_profile: effectiveHumanBehavior.profile,
1501
+ human_rest_enabled: effectiveHumanRestEnabled,
1502
+ human_rest_count: 0,
1503
+ human_rest_ms: 0,
1504
+ last_human_event: null
1505
+ },
1403
1506
  checkpoint: {},
1404
1507
  task: (runControl) => workflow({
1405
1508
  client,
@@ -1434,13 +1537,15 @@ export function createRecommendRunService({
1434
1537
  actionAfterClickDelayMs,
1435
1538
  screeningMode: normalizedScreeningMode,
1436
1539
  llmConfig,
1437
- llmTimeoutMs,
1438
- llmImageLimit,
1439
- llmImageDetail,
1440
- imageOutputDir
1441
- }, runControl)
1442
- });
1443
- }
1540
+ llmTimeoutMs,
1541
+ llmImageLimit,
1542
+ llmImageDetail,
1543
+ imageOutputDir,
1544
+ humanRestEnabled: effectiveHumanRestEnabled,
1545
+ humanBehavior: effectiveHumanBehavior
1546
+ }, runControl)
1547
+ });
1548
+ }
1444
1549
 
1445
1550
  return {
1446
1551
  startRecommendRun,
@@ -8,6 +8,12 @@ import {
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
10
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
+ import {
12
+ configureHumanInteraction,
13
+ createHumanRestController,
14
+ humanDelay,
15
+ normalizeHumanBehaviorOptions
16
+ } from "../../core/browser/index.js";
11
17
  import {
12
18
  compactCvAcquisitionState,
13
19
  countParsedNetworkProfiles,
@@ -291,9 +297,27 @@ export async function runRecruitWorkflow({
291
297
  llmTimeoutMs = 120000,
292
298
  llmImageLimit = 8,
293
299
  llmImageDetail = "high",
294
- imageOutputDir = ""
300
+ imageOutputDir = "",
301
+ humanRestEnabled = false,
302
+ humanBehavior = null
295
303
  } = {}, runControl) {
296
304
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
305
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
306
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
307
+ });
308
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
309
+ configureHumanInteraction(client, {
310
+ enabled: effectiveHumanBehavior.enabled,
311
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
312
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
313
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
314
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
315
+ });
316
+ const humanRestController = createHumanRestController({
317
+ enabled: effectiveHumanRestEnabled,
318
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
319
+ batchRestEnabled: effectiveHumanBehavior.batchRest
320
+ });
297
321
  const normalizedSearchParams = normalizeSearchParams(searchParams);
298
322
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
299
323
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
@@ -326,6 +350,7 @@ export async function runRecruitWorkflow({
326
350
  const candidateRecoveryCounts = new Map();
327
351
  let cardNodeIds = [];
328
352
  let listEndReason = "";
353
+ let lastHumanEvent = null;
329
354
  const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
330
355
  rootNodeId: rootState?.iframe?.documentNodeId,
331
356
  containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
@@ -335,9 +360,36 @@ export async function runRecruitWorkflow({
335
360
  validateViewportPoint: true
336
361
  }));
337
362
 
363
+ function recordHumanEvent(event = null) {
364
+ if (!event) return lastHumanEvent;
365
+ lastHumanEvent = {
366
+ at: new Date().toISOString(),
367
+ ...event
368
+ };
369
+ return lastHumanEvent;
370
+ }
371
+
372
+ async function maybeHumanActionCooldown(phase, timings = {}) {
373
+ if (!effectiveHumanBehavior.actionCooldown) return null;
374
+ const pauseMs = humanDelay(280, 90, {
375
+ minMs: 80,
376
+ maxMs: 720
377
+ });
378
+ if (pauseMs > 0) {
379
+ await runControl.sleep(pauseMs);
380
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
381
+ }
382
+ return recordHumanEvent({
383
+ kind: "action_cooldown",
384
+ phase,
385
+ pause_ms: pauseMs
386
+ });
387
+ }
388
+
338
389
  function updateRecruitProgress(extra = {}) {
339
390
  const counts = countRecruitResultStatuses(results);
340
391
  const listSnapshot = compactInfiniteListState(listState);
392
+ const humanRestState = humanRestController.getState();
341
393
  runControl.updateProgress({
342
394
  card_count: cardNodeIds.length,
343
395
  target_count: limit,
@@ -351,6 +403,12 @@ export async function runRecruitWorkflow({
351
403
  list_end_reason: listEndReason || null,
352
404
  viewport_checks: viewportGuard.getStats().checks,
353
405
  viewport_recoveries: viewportGuard.getStats().recoveries,
406
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
407
+ human_behavior_profile: effectiveHumanBehavior.profile,
408
+ human_rest_enabled: effectiveHumanRestEnabled,
409
+ human_rest_count: humanRestState.rest_count,
410
+ human_rest_ms: humanRestState.total_rest_ms,
411
+ last_human_event: lastHumanEvent,
354
412
  ...extra
355
413
  });
356
414
  }
@@ -519,6 +577,7 @@ export async function runRecruitWorkflow({
519
577
  stableSignatureLimit: listStableSignatureLimit,
520
578
  wheelDeltaY: listWheelDeltaY,
521
579
  settleMs: listSettleMs,
580
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
522
581
  fallbackPoint: listFallbackResolver,
523
582
  findNodeIds: async () => {
524
583
  let currentRootState = await getRecruitRoots(client);
@@ -622,6 +681,7 @@ export async function runRecruitWorkflow({
622
681
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
623
682
  detailStep = "open_detail";
624
683
  networkRecorder.clear();
684
+ await maybeHumanActionCooldown("before_detail_open", timings);
625
685
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
626
686
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
627
687
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
@@ -693,6 +753,7 @@ export async function runRecruitWorkflow({
693
753
  wheelDeltaY: imageWheelDeltaY,
694
754
  settleMs: 350,
695
755
  scrollMethod: "dom-anchor-fallback-input",
756
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
696
757
  stepTimeoutMs: 45000,
697
758
  totalTimeoutMs: 90000,
698
759
  duplicateStopCount: 1,
@@ -765,6 +826,7 @@ export async function runRecruitWorkflow({
765
826
  screeningCandidate = detailResult.candidate;
766
827
  if (closeDetail) {
767
828
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
829
+ await maybeHumanActionCooldown("after_detail_close", timings);
768
830
  if (!detailResult.close_result?.closed) {
769
831
  const closeError = createRecruitCloseFailureError(detailResult.close_result);
770
832
  const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
@@ -892,6 +954,27 @@ export async function runRecruitWorkflow({
892
954
  });
893
955
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
894
956
 
957
+ if (effectiveHumanRestEnabled) {
958
+ const restStarted = Date.now();
959
+ const restResult = await humanRestController.takeBreakIfNeeded({
960
+ sleepFn: (ms) => runControl.sleep(ms)
961
+ });
962
+ const restElapsed = Date.now() - restStarted;
963
+ if (restResult.rested) {
964
+ recordHumanEvent({
965
+ kind: "rest",
966
+ pause_ms: restResult.pause_ms || restElapsed,
967
+ events: restResult.events || []
968
+ });
969
+ compactResult.human_rest = restResult;
970
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
971
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
972
+ updateRecruitProgress({
973
+ human_rest_last: restResult
974
+ });
975
+ }
976
+ }
977
+
895
978
  if (delayMs > 0) {
896
979
  const sleepStarted = Date.now();
897
980
  await runControl.sleep(delayMs);
@@ -911,6 +994,9 @@ export async function runRecruitWorkflow({
911
994
  stats: viewportGuard.getStats(),
912
995
  events: viewportGuard.getEvents()
913
996
  },
997
+ human_behavior: effectiveHumanBehavior,
998
+ human_rest: humanRestController.getState(),
999
+ last_human_event: lastHumanEvent,
914
1000
  list_end_reason: listEndReason || null,
915
1001
  refresh_rounds: refreshRounds,
916
1002
  refresh_attempts: refreshAttempts,
@@ -958,6 +1044,8 @@ export function createRecruitRunService({
958
1044
  llmImageLimit = 8,
959
1045
  llmImageDetail = "high",
960
1046
  imageOutputDir = "",
1047
+ humanRestEnabled = false,
1048
+ humanBehavior = null,
961
1049
  name = "recruit-domain-run"
962
1050
  } = {}) {
963
1051
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
@@ -965,6 +1053,10 @@ export function createRecruitRunService({
965
1053
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
966
1054
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
967
1055
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
1056
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1057
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1058
+ });
1059
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
968
1060
  return manager.startRun({
969
1061
  name,
970
1062
  context: {
@@ -994,7 +1086,11 @@ export function createRecruitRunService({
994
1086
  llm_timeout_ms: llmTimeoutMs,
995
1087
  llm_image_limit: llmImageLimit,
996
1088
  llm_image_detail: llmImageDetail,
997
- image_output_dir: imageOutputDir || ""
1089
+ image_output_dir: imageOutputDir || "",
1090
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1091
+ human_behavior_profile: effectiveHumanBehavior.profile,
1092
+ human_behavior: effectiveHumanBehavior,
1093
+ human_rest_enabled: effectiveHumanRestEnabled
998
1094
  },
999
1095
  progress: {
1000
1096
  card_count: 0,
@@ -1007,7 +1103,13 @@ export function createRecruitRunService({
1007
1103
  image_capture_failed: 0,
1008
1104
  detail_open_failed: 0,
1009
1105
  transient_recovered: 0,
1010
- context_recoveries: 0
1106
+ context_recoveries: 0,
1107
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1108
+ human_behavior_profile: effectiveHumanBehavior.profile,
1109
+ human_rest_enabled: effectiveHumanRestEnabled,
1110
+ human_rest_count: 0,
1111
+ human_rest_ms: 0,
1112
+ last_human_event: null
1011
1113
  },
1012
1114
  checkpoint: {},
1013
1115
  task: (runControl) => workflow({
@@ -1039,7 +1141,9 @@ export function createRecruitRunService({
1039
1141
  llmTimeoutMs,
1040
1142
  llmImageLimit,
1041
1143
  llmImageDetail,
1042
- imageOutputDir
1144
+ imageOutputDir,
1145
+ humanRestEnabled: effectiveHumanRestEnabled,
1146
+ humanBehavior: effectiveHumanBehavior
1043
1147
  }, runControl)
1044
1148
  });
1045
1149
  }