@reconcrap/boss-recommend-mcp 2.1.20 → 2.1.22

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 (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2372
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +529 -499
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,7 +1,7 @@
1
- import {
2
- captureScrolledNodeScreenshots,
3
- captureViewportScreenshot
4
- } from "../../core/capture/index.js";
1
+ import {
2
+ captureScrolledNodeScreenshots,
3
+ captureViewportScreenshot
4
+ } from "../../core/capture/index.js";
5
5
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
6
6
  import {
7
7
  clickPoint,
@@ -35,22 +35,22 @@ import {
35
35
  resolveInfiniteListFallbackPoint
36
36
  } from "../../core/infinite-list/index.js";
37
37
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
38
- import {
39
- createRunLifecycleManager,
40
- RunCanceledError
41
- } from "../../core/run/index.js";
38
+ import {
39
+ createRunLifecycleManager,
40
+ RunCanceledError
41
+ } from "../../core/run/index.js";
42
42
  import {
43
43
  addTiming,
44
44
  imageEvidenceFilePath,
45
45
  measureTiming
46
46
  } from "../../core/run/timing.js";
47
- import {
48
- callScreeningLlm,
49
- createFatalLlmRunError,
50
- isFatalLlmProviderError,
51
- normalizeText,
52
- screenCandidate
53
- } from "../../core/screening/index.js";
47
+ import {
48
+ callScreeningLlm,
49
+ createFatalLlmRunError,
50
+ isFatalLlmProviderError,
51
+ normalizeText,
52
+ screenCandidate
53
+ } from "../../core/screening/index.js";
54
54
  import {
55
55
  CHAT_BOTTOM_MARKER_SELECTORS,
56
56
  CHAT_CARD_SELECTORS,
@@ -67,14 +67,14 @@ import {
67
67
  closeChatBlockingPanels,
68
68
  closeChatResumeModal,
69
69
  createChatProfileNetworkRecorder,
70
- extractChatProfileCandidate,
71
- isChatOnlineResumeModalOpenFailureError,
72
- isUnsafeChatOnlineResumeLinkError,
73
- openChatOnlineResume,
74
- quickChatResumeModalOpenProbe,
75
- readChatActiveCandidateState,
76
- readChatConversationReadyState,
77
- requestChatResumeForPassedCandidate,
70
+ extractChatProfileCandidate,
71
+ isChatOnlineResumeModalOpenFailureError,
72
+ isUnsafeChatOnlineResumeLinkError,
73
+ openChatOnlineResume,
74
+ quickChatResumeModalOpenProbe,
75
+ readChatActiveCandidateState,
76
+ readChatConversationReadyState,
77
+ requestChatResumeForPassedCandidate,
78
78
  selectChatMessageFilter,
79
79
  selectChatPrimaryLabel,
80
80
  waitForChatOnlineResumeButton,
@@ -91,6 +91,8 @@ import {
91
91
  import { getChatRoots } from "./roots.js";
92
92
 
93
93
  const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
94
+ const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS = 5000;
95
+ const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS = 8000;
94
96
 
95
97
  function normalizeDetailSource(value) {
96
98
  const normalized = String(value || "").trim().toLowerCase();
@@ -114,35 +116,35 @@ function compactScreening(screening) {
114
116
 
115
117
  function compactLlmResult(llmResult) {
116
118
  if (!llmResult) return null;
117
- return {
118
- ok: Boolean(llmResult.ok),
119
- provider: llmResult.provider || null,
120
- passed: llmResult.passed,
121
- review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
122
- cot: llmResult.cot || llmResult.decision_cot || "",
123
- reasoning_content: llmResult.reasoning_content || "",
124
- raw_model_output: llmResult.raw_model_output || "",
125
- evidence_count: llmResult.evidence?.length || 0,
126
- usage: llmResult.usage || null,
119
+ return {
120
+ ok: Boolean(llmResult.ok),
121
+ provider: llmResult.provider || null,
122
+ passed: llmResult.passed,
123
+ review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
124
+ cot: llmResult.cot || llmResult.decision_cot || "",
125
+ reasoning_content: llmResult.reasoning_content || "",
126
+ raw_model_output: llmResult.raw_model_output || "",
127
+ evidence_count: llmResult.evidence?.length || 0,
128
+ usage: llmResult.usage || null,
127
129
  finish_reason: llmResult.finish_reason || null,
128
- image_input_count: llmResult.image_input_count || 0,
129
- attempt_count: llmResult.attempt_count || 0,
130
- fallback_count: llmResult.fallback_count || 0,
131
- llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
132
- screening_strategy: llmResult.screening_strategy || "",
133
- fast_thinking_level: llmResult.fast_thinking_level || "",
134
- verify_thinking_level: llmResult.verify_thinking_level || "",
135
- verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
136
- verification_reason: llmResult.verification_reason || "",
137
- decision_source: llmResult.decision_source || "",
138
- fast_result: llmResult.fast_result || null,
139
- verify_result: llmResult.verify_result || null,
140
- error_code: llmResult.error_code || null,
141
- fatal: Boolean(llmResult.fatal),
142
- fatal_reason: llmResult.fatal_reason || "",
143
- error: llmResult.error || null
144
- };
145
- }
130
+ image_input_count: llmResult.image_input_count || 0,
131
+ attempt_count: llmResult.attempt_count || 0,
132
+ fallback_count: llmResult.fallback_count || 0,
133
+ llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
134
+ screening_strategy: llmResult.screening_strategy || "",
135
+ fast_thinking_level: llmResult.fast_thinking_level || "",
136
+ verify_thinking_level: llmResult.verify_thinking_level || "",
137
+ verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
138
+ verification_reason: llmResult.verification_reason || "",
139
+ decision_source: llmResult.decision_source || "",
140
+ fast_result: llmResult.fast_result || null,
141
+ verify_result: llmResult.verify_result || null,
142
+ error_code: llmResult.error_code || null,
143
+ fatal: Boolean(llmResult.fatal),
144
+ fatal_reason: llmResult.fatal_reason || "",
145
+ error: llmResult.error || null
146
+ };
147
+ }
146
148
 
147
149
  function compactCandidate(candidate) {
148
150
  return {
@@ -361,9 +363,9 @@ function isRecoverableLlmScreeningError(error) {
361
363
  }
362
364
 
363
365
  function createFailedLlmResult(error) {
364
- return {
365
- ok: false,
366
- passed: false,
366
+ return {
367
+ ok: false,
368
+ passed: false,
367
369
  reason: "",
368
370
  evidence: [],
369
371
  cot: "",
@@ -371,110 +373,110 @@ function createFailedLlmResult(error) {
371
373
  reasoning_content: "",
372
374
  raw_model_output: "",
373
375
  attempt_count: Number(error?.llm_attempt_count) || 0,
374
- fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
375
- llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
376
- error_code: error?.code || null,
377
- fatal: Boolean(isFatalLlmProviderError(error)),
378
- fatal_reason: error?.llm_fatal_reason || "",
379
- error: error?.message || String(error || "unknown"),
380
- screened_at: new Date().toISOString()
381
- };
382
- }
383
-
384
- function normalizeScreeningMode(value) {
385
- const normalized = String(value || "llm").trim().toLowerCase();
386
- if (["collect_cv", "collect-cv", "cv_collection", "request_cv", "request_resume"].includes(normalized)) {
387
- return "collect_cv";
388
- }
389
- return ["deterministic", "local", "local_scorer"].includes(normalized)
390
- ? "deterministic"
391
- : "llm";
392
- }
393
-
394
- function isCvAcquiredOrAvailable(detailResult = null, preActionState = null) {
395
- return Boolean(
396
- preActionState?.attachment_resume_enabled
397
- || detailResult?.cv_acquisition?.full_cv_evidence?.full_cv_acquired
398
- );
399
- }
400
-
401
- function isChatResumeRequestAvailable(preActionState = null) {
402
- return Boolean(preActionState?.ask_resume?.node_id && !preActionState.ask_resume.disabled);
403
- }
404
-
405
- function shouldSkipCvCollectionForDetailReason(reason = "") {
406
- const normalized = normalizeText(reason);
407
- return [
408
- "active_candidate_mismatch",
409
- "forbidden_top_level_resume_navigation",
410
- "online_resume_modal_did_not_open",
411
- "unsafe_online_resume_navigation_link"
412
- ].includes(normalized)
413
- || normalized.startsWith("recoverable_cdp_node_stale:")
414
- || normalized.startsWith("resume_modal_close_failed:");
415
- }
416
-
417
- export function createCvCollectionScreening(screeningCandidate, {
418
- detailResult = null,
419
- detailUnavailableReason = "",
420
- preActionState = null
421
- } = {}) {
422
- if (preActionState?.already_requested_resume) {
423
- return {
424
- status: "skip",
425
- passed: false,
426
- score: 0,
427
- reasons: ["resume_request_already_pending"],
428
- candidate: screeningCandidate
429
- };
430
- }
431
- if (isCvAcquiredOrAvailable(detailResult, preActionState)) {
432
- const reason = preActionState?.attachment_resume_enabled
433
- ? "attachment_resume_already_available"
434
- : "online_cv_already_available";
435
- return {
436
- status: "skip",
437
- passed: false,
438
- score: 0,
439
- reasons: [reason],
440
- candidate: screeningCandidate
441
- };
442
- }
443
- if (isChatResumeRequestAvailable(preActionState)) {
444
- const reason = detailUnavailableReason || "request_cv_available";
445
- return {
446
- status: "pass",
447
- passed: true,
448
- score: 100,
449
- reasons: [`collect_cv:${reason}`],
450
- candidate: screeningCandidate
451
- };
452
- }
453
- if (shouldSkipCvCollectionForDetailReason(detailUnavailableReason)) {
454
- return {
455
- status: "skip",
456
- passed: false,
457
- score: 0,
458
- reasons: [detailUnavailableReason],
459
- candidate: screeningCandidate
460
- };
461
- }
462
- const reason = detailUnavailableReason || "cv_collection_missing_online_cv";
463
- return {
464
- status: "pass",
465
- passed: true,
466
- score: 100,
467
- reasons: [`collect_cv:${reason}`],
468
- candidate: screeningCandidate
469
- };
470
- }
471
-
472
- export function shouldOpenOnlineResumeForChatDetail({
473
- collectCvOnly = false,
474
- detailResult = null
475
- } = {}) {
476
- return !collectCvOnly && !detailResult;
477
- }
376
+ fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
377
+ llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
378
+ error_code: error?.code || null,
379
+ fatal: Boolean(isFatalLlmProviderError(error)),
380
+ fatal_reason: error?.llm_fatal_reason || "",
381
+ error: error?.message || String(error || "unknown"),
382
+ screened_at: new Date().toISOString()
383
+ };
384
+ }
385
+
386
+ function normalizeScreeningMode(value) {
387
+ const normalized = String(value || "llm").trim().toLowerCase();
388
+ if (["collect_cv", "collect-cv", "cv_collection", "request_cv", "request_resume"].includes(normalized)) {
389
+ return "collect_cv";
390
+ }
391
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
392
+ ? "deterministic"
393
+ : "llm";
394
+ }
395
+
396
+ function isCvAcquiredOrAvailable(detailResult = null, preActionState = null) {
397
+ return Boolean(
398
+ preActionState?.attachment_resume_enabled
399
+ || detailResult?.cv_acquisition?.full_cv_evidence?.full_cv_acquired
400
+ );
401
+ }
402
+
403
+ function isChatResumeRequestAvailable(preActionState = null) {
404
+ return Boolean(preActionState?.ask_resume?.node_id && !preActionState.ask_resume.disabled);
405
+ }
406
+
407
+ function shouldSkipCvCollectionForDetailReason(reason = "") {
408
+ const normalized = normalizeText(reason);
409
+ return [
410
+ "active_candidate_mismatch",
411
+ "forbidden_top_level_resume_navigation",
412
+ "online_resume_modal_did_not_open",
413
+ "unsafe_online_resume_navigation_link"
414
+ ].includes(normalized)
415
+ || normalized.startsWith("recoverable_cdp_node_stale:")
416
+ || normalized.startsWith("resume_modal_close_failed:");
417
+ }
418
+
419
+ export function createCvCollectionScreening(screeningCandidate, {
420
+ detailResult = null,
421
+ detailUnavailableReason = "",
422
+ preActionState = null
423
+ } = {}) {
424
+ if (preActionState?.already_requested_resume) {
425
+ return {
426
+ status: "skip",
427
+ passed: false,
428
+ score: 0,
429
+ reasons: ["resume_request_already_pending"],
430
+ candidate: screeningCandidate
431
+ };
432
+ }
433
+ if (isCvAcquiredOrAvailable(detailResult, preActionState)) {
434
+ const reason = preActionState?.attachment_resume_enabled
435
+ ? "attachment_resume_already_available"
436
+ : "online_cv_already_available";
437
+ return {
438
+ status: "skip",
439
+ passed: false,
440
+ score: 0,
441
+ reasons: [reason],
442
+ candidate: screeningCandidate
443
+ };
444
+ }
445
+ if (isChatResumeRequestAvailable(preActionState)) {
446
+ const reason = detailUnavailableReason || "request_cv_available";
447
+ return {
448
+ status: "pass",
449
+ passed: true,
450
+ score: 100,
451
+ reasons: [`collect_cv:${reason}`],
452
+ candidate: screeningCandidate
453
+ };
454
+ }
455
+ if (shouldSkipCvCollectionForDetailReason(detailUnavailableReason)) {
456
+ return {
457
+ status: "skip",
458
+ passed: false,
459
+ score: 0,
460
+ reasons: [detailUnavailableReason],
461
+ candidate: screeningCandidate
462
+ };
463
+ }
464
+ const reason = detailUnavailableReason || "cv_collection_missing_online_cv";
465
+ return {
466
+ status: "pass",
467
+ passed: true,
468
+ score: 100,
469
+ reasons: [`collect_cv:${reason}`],
470
+ candidate: screeningCandidate
471
+ };
472
+ }
473
+
474
+ export function shouldOpenOnlineResumeForChatDetail({
475
+ collectCvOnly = false,
476
+ detailResult = null
477
+ } = {}) {
478
+ return !collectCvOnly && !detailResult;
479
+ }
478
480
 
479
481
  function createMissingLlmConfigResult() {
480
482
  return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
@@ -496,81 +498,81 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
496
498
  };
497
499
  }
498
500
 
499
- function compactChatRuntimeError(error) {
500
- if (!error) return null;
501
- return {
502
- name: error.name || "Error",
503
- code: error.code || null,
504
- message: error.message || String(error),
505
- retryable: typeof error.retryable === "boolean" ? error.retryable : null,
506
- attempts: Array.isArray(error.attempts) ? error.attempts : null,
507
- close_result: error.close_result || null,
508
- selection_ready_state: error.selection_ready_state || null,
509
- page_state: error.page_state || null
510
- };
511
- }
512
-
513
- async function captureChatFinalFailureArtifact(client, {
514
- runControl,
515
- imageOutputDir = "",
516
- error = null
517
- } = {}) {
518
- if (!client || !imageOutputDir || !runControl?.runId) return null;
519
- const artifact = {
520
- schema_version: 1,
521
- kind: "chat_final_failure_page",
522
- captured_at: new Date().toISOString(),
523
- run_id: runControl.runId,
524
- error: compactChatRuntimeError(error),
525
- page_state: null,
526
- active_candidate_state: null,
527
- conversation_ready_state: null,
528
- screenshot: null,
529
- screenshot_error: null
530
- };
531
- try {
532
- artifact.page_state = await getChatTopLevelState(client);
533
- } catch (pageError) {
534
- artifact.page_state = {
535
- error: pageError?.message || String(pageError)
536
- };
537
- }
538
- try {
539
- artifact.active_candidate_state = await readChatActiveCandidateState(client);
540
- } catch (activeCandidateError) {
541
- artifact.active_candidate_state = {
542
- error: activeCandidateError?.message || String(activeCandidateError)
543
- };
544
- }
545
- try {
546
- artifact.conversation_ready_state = await readChatConversationReadyState(client);
547
- } catch (conversationError) {
548
- artifact.conversation_ready_state = {
549
- error: conversationError?.message || String(conversationError)
550
- };
551
- }
552
- try {
553
- artifact.screenshot = await captureViewportScreenshot(client, {
554
- filePath: imageEvidenceFilePath({
555
- imageOutputDir,
556
- domain: "chat-final-failure",
557
- runId: runControl.runId,
558
- index: 0,
559
- extension: "jpg"
560
- }),
561
- format: "jpeg",
562
- quality: 72,
563
- metadata: {
564
- domain: "chat",
565
- run_id: runControl.runId,
566
- reason: "final_failure"
567
- }
568
- });
569
- } catch (screenshotError) {
570
- artifact.screenshot_error = screenshotError?.message || String(screenshotError);
571
- }
572
- return artifact;
573
- }
501
+ function compactChatRuntimeError(error) {
502
+ if (!error) return null;
503
+ return {
504
+ name: error.name || "Error",
505
+ code: error.code || null,
506
+ message: error.message || String(error),
507
+ retryable: typeof error.retryable === "boolean" ? error.retryable : null,
508
+ attempts: Array.isArray(error.attempts) ? error.attempts : null,
509
+ close_result: error.close_result || null,
510
+ selection_ready_state: error.selection_ready_state || null,
511
+ page_state: error.page_state || null
512
+ };
513
+ }
514
+
515
+ async function captureChatFinalFailureArtifact(client, {
516
+ runControl,
517
+ imageOutputDir = "",
518
+ error = null
519
+ } = {}) {
520
+ if (!client || !imageOutputDir || !runControl?.runId) return null;
521
+ const artifact = {
522
+ schema_version: 1,
523
+ kind: "chat_final_failure_page",
524
+ captured_at: new Date().toISOString(),
525
+ run_id: runControl.runId,
526
+ error: compactChatRuntimeError(error),
527
+ page_state: null,
528
+ active_candidate_state: null,
529
+ conversation_ready_state: null,
530
+ screenshot: null,
531
+ screenshot_error: null
532
+ };
533
+ try {
534
+ artifact.page_state = await getChatTopLevelState(client);
535
+ } catch (pageError) {
536
+ artifact.page_state = {
537
+ error: pageError?.message || String(pageError)
538
+ };
539
+ }
540
+ try {
541
+ artifact.active_candidate_state = await readChatActiveCandidateState(client);
542
+ } catch (activeCandidateError) {
543
+ artifact.active_candidate_state = {
544
+ error: activeCandidateError?.message || String(activeCandidateError)
545
+ };
546
+ }
547
+ try {
548
+ artifact.conversation_ready_state = await readChatConversationReadyState(client);
549
+ } catch (conversationError) {
550
+ artifact.conversation_ready_state = {
551
+ error: conversationError?.message || String(conversationError)
552
+ };
553
+ }
554
+ try {
555
+ artifact.screenshot = await captureViewportScreenshot(client, {
556
+ filePath: imageEvidenceFilePath({
557
+ imageOutputDir,
558
+ domain: "chat-final-failure",
559
+ runId: runControl.runId,
560
+ index: 0,
561
+ extension: "jpg"
562
+ }),
563
+ format: "jpeg",
564
+ quality: 72,
565
+ metadata: {
566
+ domain: "chat",
567
+ run_id: runControl.runId,
568
+ reason: "final_failure"
569
+ }
570
+ });
571
+ } catch (screenshotError) {
572
+ artifact.screenshot_error = screenshotError?.message || String(screenshotError);
573
+ }
574
+ return artifact;
575
+ }
574
576
 
575
577
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
576
578
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
@@ -738,15 +740,15 @@ async function resolveFreshChatCardNodeId(client, {
738
740
  return freshNodeId || fallbackNodeId;
739
741
  }
740
742
 
741
- async function selectFreshChatCandidate(client, {
742
- cardNodeId,
743
- candidate,
744
- timeoutMs,
745
- settleMs = 1200,
746
- onlineResumeProbe = true
747
- } = {}) {
748
- let lastError = null;
749
- for (let attempt = 0; attempt < 3; attempt += 1) {
743
+ async function selectFreshChatCandidate(client, {
744
+ cardNodeId,
745
+ candidate,
746
+ timeoutMs,
747
+ settleMs = 1200,
748
+ onlineResumeProbe = true
749
+ } = {}) {
750
+ let lastError = null;
751
+ for (let attempt = 0; attempt < 3; attempt += 1) {
750
752
  const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
751
753
  const rootState = await getChatRoots(client);
752
754
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
@@ -757,18 +759,18 @@ async function selectFreshChatCandidate(client, {
757
759
  try {
758
760
  await scrollNodeIntoView(client, freshNodeId);
759
761
  await sleep(250);
760
- const box = await getNodeBox(client, freshNodeId);
761
- await clickPoint(client, box.center.x, box.center.y);
762
- if (settleMs > 0) await sleep(settleMs);
763
- const ready = onlineResumeProbe
764
- ? await waitForChatOnlineResumeButton(client, {
765
- timeoutMs,
766
- expectedCandidateId: candidate?.id || ""
767
- })
768
- : await readSelectedChatCandidateState(client, candidate);
769
- return {
770
- card_box: box,
771
- ready,
762
+ const box = await getNodeBox(client, freshNodeId);
763
+ await clickPoint(client, box.center.x, box.center.y);
764
+ if (settleMs > 0) await sleep(settleMs);
765
+ const ready = onlineResumeProbe
766
+ ? await waitForChatOnlineResumeButton(client, {
767
+ timeoutMs,
768
+ expectedCandidateId: candidate?.id || ""
769
+ })
770
+ : await readSelectedChatCandidateState(client, candidate);
771
+ return {
772
+ card_box: box,
773
+ ready,
772
774
  card_node_id: freshNodeId,
773
775
  refreshed_node: freshNodeId !== cardNodeId,
774
776
  modal_guard: modalGuard,
@@ -780,35 +782,35 @@ async function selectFreshChatCandidate(client, {
780
782
  await sleep(350);
781
783
  }
782
784
  }
783
- throw lastError || new Error("Chat candidate selection failed");
784
- }
785
-
786
- async function readSelectedChatCandidateState(client, candidate = null) {
787
- const topLevelState = await getChatTopLevelState(client);
788
- if (topLevelState.is_forbidden_resume_top_level) {
789
- return {
790
- forbidden_top_level_navigation: true,
791
- top_level_state: topLevelState
792
- };
793
- }
794
- const activeState = await readChatActiveCandidateState(client);
795
- const expectedId = normalizeText(candidate?.id || "");
796
- const activeCandidateId = normalizeText(activeState?.active_candidate?.candidate_id || "");
797
- const candidateSelectionVerified = expectedId
798
- ? activeCandidateId === expectedId
799
- : undefined;
800
- return {
801
- ok: !expectedId || candidateSelectionVerified === true,
802
- reason: expectedId && candidateSelectionVerified !== true
803
- ? "active_candidate_mismatch"
804
- : "online_resume_probe_skipped",
805
- roots: activeState.roots,
806
- activeCandidate: activeState.active_candidate,
807
- expected_candidate_id: expectedId || null,
808
- active_candidate_id: activeCandidateId || null,
809
- candidate_selection_verified: candidateSelectionVerified
810
- };
811
- }
785
+ throw lastError || new Error("Chat candidate selection failed");
786
+ }
787
+
788
+ async function readSelectedChatCandidateState(client, candidate = null) {
789
+ const topLevelState = await getChatTopLevelState(client);
790
+ if (topLevelState.is_forbidden_resume_top_level) {
791
+ return {
792
+ forbidden_top_level_navigation: true,
793
+ top_level_state: topLevelState
794
+ };
795
+ }
796
+ const activeState = await readChatActiveCandidateState(client);
797
+ const expectedId = normalizeText(candidate?.id || "");
798
+ const activeCandidateId = normalizeText(activeState?.active_candidate?.candidate_id || "");
799
+ const candidateSelectionVerified = expectedId
800
+ ? activeCandidateId === expectedId
801
+ : undefined;
802
+ return {
803
+ ok: !expectedId || candidateSelectionVerified === true,
804
+ reason: expectedId && candidateSelectionVerified !== true
805
+ ? "active_candidate_mismatch"
806
+ : "online_resume_probe_skipped",
807
+ roots: activeState.roots,
808
+ activeCandidate: activeState.active_candidate,
809
+ expected_candidate_id: expectedId || null,
810
+ active_candidate_id: activeCandidateId || null,
811
+ candidate_selection_verified: candidateSelectionVerified
812
+ };
813
+ }
812
814
 
813
815
  function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
814
816
  if (detailSource !== "network" && detailSource !== "cascade") return [];
@@ -942,7 +944,12 @@ export async function runChatWorkflow({
942
944
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
943
945
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
944
946
  });
945
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
947
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
948
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
949
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
950
+ const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
951
+ const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
952
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
946
953
  configureHumanInteraction(client, {
947
954
  enabled: effectiveHumanBehavior.enabled,
948
955
  clickMovementEnabled: effectiveHumanBehavior.clickMovement,
@@ -950,16 +957,15 @@ export async function runChatWorkflow({
950
957
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
951
958
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
952
959
  });
953
- const humanRestController = createHumanRestController({
954
- enabled: effectiveHumanRestEnabled,
955
- shortRestEnabled: effectiveHumanBehavior.shortRest,
956
- batchRestEnabled: effectiveHumanBehavior.batchRest,
957
- restLevel: effectiveHumanBehavior.restLevel
958
- });
959
- const normalizedDetailSource = normalizeDetailSource(detailSource);
960
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
961
- const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
962
- const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
960
+ const humanRestController = createHumanRestController({
961
+ enabled: effectiveHumanRestEnabled,
962
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
963
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
964
+ restLevel: effectiveHumanBehavior.restLevel,
965
+ perCandidateRestEnabled: collectCvPerCandidateRestEnabled,
966
+ perCandidateRestMinMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS,
967
+ perCandidateRestMaxMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS
968
+ });
963
969
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
964
970
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
965
971
  ? Number(targetPassCount)
@@ -1000,12 +1006,12 @@ export async function runChatWorkflow({
1000
1006
  let requestedCount = 0;
1001
1007
  let requestSatisfiedCount = 0;
1002
1008
  let requestSkippedCount = 0;
1003
- let contextSetup = {};
1004
- let contextRecoveryAttempts = 0;
1005
- const candidateRecoveryCounts = new Map();
1006
- let lastHumanEvent = null;
1007
-
1008
- function recordHumanEvent(event = null) {
1009
+ let contextSetup = {};
1010
+ let contextRecoveryAttempts = 0;
1011
+ const candidateRecoveryCounts = new Map();
1012
+ let lastHumanEvent = null;
1013
+
1014
+ function recordHumanEvent(event = null) {
1009
1015
  if (!event) return lastHumanEvent;
1010
1016
  lastHumanEvent = {
1011
1017
  at: new Date().toISOString(),
@@ -1069,13 +1075,13 @@ export async function runChatWorkflow({
1069
1075
  ...setup.contextSetup,
1070
1076
  initial_top_level_state: initialTopLevelState
1071
1077
  };
1072
- runControl.checkpoint({
1073
- chat_context: contextSetup
1074
- });
1075
-
1076
- async function recoverAndReapplyChatContext(reason, error = null, {
1077
- forceRefresh = false
1078
- } = {}) {
1078
+ runControl.checkpoint({
1079
+ chat_context: contextSetup
1080
+ });
1081
+
1082
+ async function recoverAndReapplyChatContext(reason, error = null, {
1083
+ forceRefresh = false
1084
+ } = {}) {
1079
1085
  runControl.setPhase("chat:recover_shell");
1080
1086
  contextRecoveryAttempts += 1;
1081
1087
  const shellRecovery = await recoverChatShell(client, {
@@ -1180,6 +1186,9 @@ export async function runChatWorkflow({
1180
1186
  human_behavior_profile: effectiveHumanBehavior.profile,
1181
1187
  human_rest_level: effectiveHumanBehavior.restLevel,
1182
1188
  human_rest_enabled: effectiveHumanRestEnabled,
1189
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1190
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1191
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1183
1192
  human_rest_count: humanRestController.getState().rest_count,
1184
1193
  human_rest_ms: humanRestController.getState().total_rest_ms,
1185
1194
  last_human_event: lastHumanEvent
@@ -1205,10 +1214,10 @@ export async function runChatWorkflow({
1205
1214
  last_human_event: lastHumanEvent,
1206
1215
  list_end_reason: listEndReason,
1207
1216
  target_pass_count: passTarget,
1208
- process_until_list_end: Boolean(processUntilListEnd),
1209
- processed_limit: processedLimit,
1210
- detail_source: normalizedDetailSource,
1211
- processed: 0,
1217
+ process_until_list_end: Boolean(processUntilListEnd),
1218
+ processed_limit: processedLimit,
1219
+ detail_source: normalizedDetailSource,
1220
+ processed: 0,
1212
1221
  screened: 0,
1213
1222
  detail_opened: 0,
1214
1223
  llm_screened: 0,
@@ -1241,7 +1250,17 @@ export async function runChatWorkflow({
1241
1250
  scroll_count: 0,
1242
1251
  context_recoveries: contextRecoveryAttempts,
1243
1252
  viewport_checks: viewportGuard.getStats().checks,
1244
- viewport_recoveries: viewportGuard.getStats().recoveries
1253
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1254
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1255
+ human_behavior_profile: effectiveHumanBehavior.profile,
1256
+ human_rest_level: effectiveHumanBehavior.restLevel,
1257
+ human_rest_enabled: effectiveHumanRestEnabled,
1258
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1259
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1260
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1261
+ human_rest_count: humanRestController.getState().rest_count,
1262
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1263
+ last_human_event: lastHumanEvent
1245
1264
  });
1246
1265
 
1247
1266
  while (
@@ -1405,12 +1424,12 @@ export async function runChatWorkflow({
1405
1424
  detailStep = "select_candidate";
1406
1425
  networkRecorder.clear();
1407
1426
  await maybeHumanActionCooldown("before_detail_open", timings);
1408
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1409
- cardNodeId,
1410
- candidate: cardCandidate,
1411
- timeoutMs: onlineResumeButtonTimeoutMs,
1412
- onlineResumeProbe: !collectCvOnly
1413
- }));
1427
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1428
+ cardNodeId,
1429
+ candidate: cardCandidate,
1430
+ timeoutMs: onlineResumeButtonTimeoutMs,
1431
+ onlineResumeProbe: !collectCvOnly
1432
+ }));
1414
1433
  if (selected.ready?.forbidden_top_level_navigation) {
1415
1434
  throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1416
1435
  }
@@ -1430,11 +1449,11 @@ export async function runChatWorkflow({
1430
1449
  detailResult.cv_acquisition.pre_detail_state = preActionState;
1431
1450
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
1432
1451
  }
1433
- if (!selected.ready?.ok) {
1434
- if (detailResult) {
1435
- // Already classified by the pre-detail conversation state.
1436
- } else if (selected.ready?.reason === "active_candidate_mismatch") {
1437
- throw makeChatCandidateSelectionMismatchError(selected, cardCandidate);
1452
+ if (!selected.ready?.ok) {
1453
+ if (detailResult) {
1454
+ // Already classified by the pre-detail conversation state.
1455
+ } else if (selected.ready?.reason === "active_candidate_mismatch") {
1456
+ throw makeChatCandidateSelectionMismatchError(selected, cardCandidate);
1438
1457
  } else {
1439
1458
  detailStep = "read_conversation_ready_state";
1440
1459
  if (preActionState.attachment_resume_enabled) {
@@ -1444,18 +1463,18 @@ export async function runChatWorkflow({
1444
1463
  } else {
1445
1464
  detailUnavailableReason = "online_resume_button_unavailable";
1446
1465
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1447
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1448
- }
1449
- }
1450
- }
1451
- if (collectCvOnly && !detailResult) {
1452
- detailUnavailableReason = preActionState?.has_online_resume
1453
- ? "collect_cv_request_candidate"
1454
- : "collect_cv_missing_online_resume";
1455
- }
1456
-
1457
- if (shouldOpenOnlineResumeForChatDetail({ collectCvOnly, detailResult })) {
1458
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1466
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1467
+ }
1468
+ }
1469
+ }
1470
+ if (collectCvOnly && !detailResult) {
1471
+ detailUnavailableReason = preActionState?.has_online_resume
1472
+ ? "collect_cv_request_candidate"
1473
+ : "collect_cv_missing_online_resume";
1474
+ }
1475
+
1476
+ if (shouldOpenOnlineResumeForChatDetail({ collectCvOnly, detailResult })) {
1477
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1459
1478
  let networkWait = null;
1460
1479
  let contentWait = {
1461
1480
  ok: false,
@@ -1715,24 +1734,24 @@ export async function runChatWorkflow({
1715
1734
  llmResult = createMissingLlmConfigResult();
1716
1735
  } else {
1717
1736
  try {
1718
- llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1719
- candidate: detailResult.candidate,
1720
- criteria,
1721
- config: llmConfig,
1722
- timeoutMs: llmTimeoutMs,
1737
+ llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1738
+ candidate: detailResult.candidate,
1739
+ criteria,
1740
+ config: llmConfig,
1741
+ timeoutMs: llmTimeoutMs,
1723
1742
  imageEvidence,
1724
1743
  maxImages: llmImageLimit,
1725
1744
  imageDetail: llmImageDetail
1726
1745
  }));
1727
- } catch (error) {
1728
- if (isFatalLlmProviderError(error)) {
1729
- throw createFatalLlmRunError(error, {
1730
- domain: "chat",
1731
- candidate: detailResult.candidate
1732
- });
1733
- }
1734
- llmResult = createFailedLlmResult(error);
1735
- }
1746
+ } catch (error) {
1747
+ if (isFatalLlmProviderError(error)) {
1748
+ throw createFatalLlmRunError(error, {
1749
+ domain: "chat",
1750
+ candidate: detailResult.candidate
1751
+ });
1752
+ }
1753
+ llmResult = createFailedLlmResult(error);
1754
+ }
1736
1755
  }
1737
1756
  }
1738
1757
  } else {
@@ -1794,24 +1813,24 @@ export async function runChatWorkflow({
1794
1813
  const llmTimingKey = imageEvidence?.file_paths?.length
1795
1814
  ? "vision_model_ms"
1796
1815
  : "text_model_ms";
1797
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1798
- candidate: detailResult.candidate,
1799
- criteria,
1800
- config: llmConfig,
1816
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1817
+ candidate: detailResult.candidate,
1818
+ criteria,
1819
+ config: llmConfig,
1801
1820
  timeoutMs: llmTimeoutMs,
1802
1821
  imageEvidence,
1803
1822
  maxImages: llmImageLimit,
1804
1823
  imageDetail: llmImageDetail
1805
1824
  }));
1806
- } catch (error) {
1807
- if (isFatalLlmProviderError(error)) {
1808
- throw createFatalLlmRunError(error, {
1809
- domain: "chat",
1810
- candidate: detailResult.candidate
1811
- });
1812
- }
1813
- llmResult = createFailedLlmResult(error);
1814
- }
1825
+ } catch (error) {
1826
+ if (isFatalLlmProviderError(error)) {
1827
+ throw createFatalLlmRunError(error, {
1828
+ domain: "chat",
1829
+ candidate: detailResult.candidate
1830
+ });
1831
+ }
1832
+ llmResult = createFailedLlmResult(error);
1833
+ }
1815
1834
  }
1816
1835
  }
1817
1836
  }
@@ -1885,11 +1904,11 @@ export async function runChatWorkflow({
1885
1904
  recovery
1886
1905
  });
1887
1906
  continue;
1888
- } else if (isChatCandidateSelectionMismatchError(error)) {
1889
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1890
- if (retryCount < 1) {
1891
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1892
- const recovery = await recoverAndReapplyChatContext(
1907
+ } else if (isChatCandidateSelectionMismatchError(error)) {
1908
+ const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1909
+ if (retryCount < 1) {
1910
+ candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1911
+ const recovery = await recoverAndReapplyChatContext(
1893
1912
  "active_candidate_mismatch",
1894
1913
  error,
1895
1914
  { forceRefresh: true }
@@ -1901,35 +1920,35 @@ export async function runChatWorkflow({
1901
1920
  continue;
1902
1921
  }
1903
1922
  detailUnavailableReason = "active_candidate_mismatch";
1904
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1905
- detailResult.cv_acquisition.selection_ready_state = error.selection_ready_state || null;
1906
- detailResult.cv_acquisition.recovery_attempted = true;
1907
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1908
- } else if (isChatOnlineResumeModalOpenFailureError(error)) {
1909
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1910
- if (retryCount < 1) {
1911
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1912
- const recovery = await recoverAndReapplyChatContext(
1913
- "online_resume_modal_did_not_open",
1914
- error,
1915
- { forceRefresh: true }
1916
- );
1917
- checkpointInProgressCandidate({
1918
- event: "retry_after_online_resume_modal_open_failure",
1919
- recovery
1920
- });
1921
- continue;
1922
- }
1923
- detailUnavailableReason = "online_resume_modal_did_not_open";
1924
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1925
- detailResult.cv_acquisition.attempts = error.attempts || null;
1926
- detailResult.cv_acquisition.recovery_attempted = true;
1927
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1928
- } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1929
- detailUnavailableReason = "unsafe_online_resume_navigation_link";
1930
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1931
- detailResult.cv_acquisition.blocked_pre_click = true;
1932
- detailResult.cv_acquisition.button_href = error.href || null;
1923
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1924
+ detailResult.cv_acquisition.selection_ready_state = error.selection_ready_state || null;
1925
+ detailResult.cv_acquisition.recovery_attempted = true;
1926
+ detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1927
+ } else if (isChatOnlineResumeModalOpenFailureError(error)) {
1928
+ const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1929
+ if (retryCount < 1) {
1930
+ candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1931
+ const recovery = await recoverAndReapplyChatContext(
1932
+ "online_resume_modal_did_not_open",
1933
+ error,
1934
+ { forceRefresh: true }
1935
+ );
1936
+ checkpointInProgressCandidate({
1937
+ event: "retry_after_online_resume_modal_open_failure",
1938
+ recovery
1939
+ });
1940
+ continue;
1941
+ }
1942
+ detailUnavailableReason = "online_resume_modal_did_not_open";
1943
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1944
+ detailResult.cv_acquisition.attempts = error.attempts || null;
1945
+ detailResult.cv_acquisition.recovery_attempted = true;
1946
+ detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1947
+ } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1948
+ detailUnavailableReason = "unsafe_online_resume_navigation_link";
1949
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1950
+ detailResult.cv_acquisition.blocked_pre_click = true;
1951
+ detailResult.cv_acquisition.button_href = error.href || null;
1933
1952
  detailResult.cv_acquisition.button_selector = error.button_selector || null;
1934
1953
  detailResult.cv_acquisition.attempts = error.attempts || null;
1935
1954
  } else {
@@ -1940,43 +1959,43 @@ export async function runChatWorkflow({
1940
1959
  await closeChatBlockingPanels(client, { attemptsLimit: 2 });
1941
1960
  }
1942
1961
  }
1943
- screeningCandidate = detailResult?.candidate || cardCandidate;
1944
- }
1962
+ screeningCandidate = detailResult?.candidate || cardCandidate;
1963
+ }
1945
1964
 
1946
1965
  await runControl.waitIfPaused();
1947
1966
  runControl.throwIfCanceled();
1948
1967
  runControl.setPhase("chat:screening");
1949
1968
  let cardOnlyLlmResult = null;
1950
- if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1951
- detailUnavailableReason = detailResult
1952
- ? "full_cv_not_acquired"
1953
- : "detail_not_opened_full_cv_required";
1954
- }
1955
- const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1956
- const screening = collectCvOnly
1957
- ? createCvCollectionScreening(screeningCandidate, {
1958
- detailResult,
1959
- detailUnavailableReason,
1960
- preActionState
1961
- })
1962
- : detailUnavailableReason
1963
- ? {
1964
- status: "skip",
1965
- passed: false,
1966
- score: 0,
1967
- reasons: [detailUnavailableReason],
1968
- candidate: screeningCandidate
1969
- }
1970
- : useLlmScreening
1971
- ? llmToScreening(effectiveLlmResult, screeningCandidate)
1972
- : screenCandidate(screeningCandidate, { criteria });
1969
+ if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1970
+ detailUnavailableReason = detailResult
1971
+ ? "full_cv_not_acquired"
1972
+ : "detail_not_opened_full_cv_required";
1973
+ }
1974
+ const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1975
+ const screening = collectCvOnly
1976
+ ? createCvCollectionScreening(screeningCandidate, {
1977
+ detailResult,
1978
+ detailUnavailableReason,
1979
+ preActionState
1980
+ })
1981
+ : detailUnavailableReason
1982
+ ? {
1983
+ status: "skip",
1984
+ passed: false,
1985
+ score: 0,
1986
+ reasons: [detailUnavailableReason],
1987
+ candidate: screeningCandidate
1988
+ }
1989
+ : useLlmScreening
1990
+ ? llmToScreening(effectiveLlmResult, screeningCandidate)
1991
+ : screenCandidate(screeningCandidate, { criteria });
1973
1992
  let postAction = null;
1974
1993
  if (requestResumeForPassed && screening.passed) {
1975
1994
  await maybeHumanActionCooldown("before_post_action", timings);
1976
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1977
- greetingText,
1978
- dryRun: dryRunRequestCv
1979
- }));
1995
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1996
+ greetingText,
1997
+ dryRun: dryRunRequestCv
1998
+ }));
1980
1999
  if (postAction?.requested) requestSatisfiedCount += 1;
1981
2000
  if (postAction?.skipped) requestSkippedCount += 1;
1982
2001
  if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
@@ -2030,6 +2049,9 @@ export async function runChatWorkflow({
2030
2049
  human_behavior_profile: effectiveHumanBehavior.profile,
2031
2050
  human_rest_level: effectiveHumanBehavior.restLevel,
2032
2051
  human_rest_enabled: effectiveHumanRestEnabled,
2052
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2053
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2054
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2033
2055
  human_rest_count: humanRestController.getState().rest_count,
2034
2056
  human_rest_ms: humanRestController.getState().total_rest_ms,
2035
2057
  last_human_event: lastHumanEvent,
@@ -2073,6 +2095,7 @@ export async function runChatWorkflow({
2073
2095
  runControl.updateProgress({
2074
2096
  human_rest_enabled: effectiveHumanRestEnabled,
2075
2097
  human_rest_level: effectiveHumanBehavior.restLevel,
2098
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2076
2099
  human_rest_count: humanRestController.getState().rest_count,
2077
2100
  human_rest_ms: humanRestController.getState().total_rest_ms,
2078
2101
  human_rest_last: restResult,
@@ -2107,10 +2130,10 @@ export async function runChatWorkflow({
2107
2130
  last_human_event: lastHumanEvent,
2108
2131
  list_end_reason: listEndReason || null,
2109
2132
  target_pass_count: passTarget,
2110
- process_until_list_end: Boolean(processUntilListEnd),
2111
- processed_limit: processedLimit,
2112
- detail_source: normalizedDetailSource,
2113
- processed: finalCounters.processed,
2133
+ process_until_list_end: Boolean(processUntilListEnd),
2134
+ processed_limit: processedLimit,
2135
+ detail_source: normalizedDetailSource,
2136
+ processed: finalCounters.processed,
2114
2137
  screened: finalCounters.screened,
2115
2138
  detail_opened: finalCounters.detail_opened,
2116
2139
  llm_screened: finalCounters.llm_screened,
@@ -2173,14 +2196,16 @@ export function createChatRunService({
2173
2196
  name = "chat-domain-run"
2174
2197
  } = {}) {
2175
2198
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
2176
- const normalizedDetailSource = normalizeDetailSource(detailSource);
2177
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2199
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
2200
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2201
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
2178
2202
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2179
2203
  const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2180
2204
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2181
2205
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2182
2206
  });
2183
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
2207
+ const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
2208
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
2184
2209
  return manager.startRun({
2185
2210
  runId,
2186
2211
  name,
@@ -2200,10 +2225,10 @@ export function createChatRunService({
2200
2225
  dry_run_request_cv: Boolean(dryRunRequestCv),
2201
2226
  greeting_text: greetingText,
2202
2227
  cv_acquisition_mode: cvAcquisitionMode,
2203
- call_llm_on_image: Boolean(callLlmOnImage),
2204
- screening_mode: normalizedScreeningMode,
2205
- cv_collection_mode: normalizedScreeningMode === "collect_cv",
2206
- llm_configured: Boolean(llmConfig),
2228
+ call_llm_on_image: Boolean(callLlmOnImage),
2229
+ screening_mode: normalizedScreeningMode,
2230
+ cv_collection_mode: normalizedScreeningMode === "collect_cv",
2231
+ llm_configured: Boolean(llmConfig),
2207
2232
  llm_timeout_ms: llmTimeoutMs,
2208
2233
  llm_image_limit: llmImageLimit,
2209
2234
  llm_image_detail: llmImageDetail,
@@ -2218,11 +2243,13 @@ export function createChatRunService({
2218
2243
  image_output_dir: imageOutputDir || "",
2219
2244
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2220
2245
  human_behavior_profile: effectiveHumanBehavior.profile,
2221
- human_behavior: effectiveHumanBehavior,
2222
- human_rest_level: effectiveHumanBehavior.restLevel,
2223
- human_rest_enabled: effectiveHumanRestEnabled,
2224
- cv_collection_mode: normalizedScreeningMode === "collect_cv"
2225
- },
2246
+ human_behavior: effectiveHumanBehavior,
2247
+ human_rest_level: effectiveHumanBehavior.restLevel,
2248
+ human_rest_enabled: effectiveHumanRestEnabled,
2249
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2250
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2251
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null
2252
+ },
2226
2253
  progress: {
2227
2254
  card_count: 0,
2228
2255
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -2242,68 +2269,71 @@ export function createChatRunService({
2242
2269
  human_behavior_profile: effectiveHumanBehavior.profile,
2243
2270
  human_rest_level: effectiveHumanBehavior.restLevel,
2244
2271
  human_rest_enabled: effectiveHumanRestEnabled,
2272
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2273
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2274
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2245
2275
  human_rest_count: 0,
2246
2276
  human_rest_ms: 0,
2247
2277
  last_human_event: null
2248
2278
  },
2249
- checkpoint: {},
2250
- task: async (runControl) => {
2251
- try {
2252
- return await workflow({
2253
- client,
2254
- targetUrl,
2255
- job,
2256
- startFrom,
2257
- criteria,
2258
- maxCandidates,
2259
- targetPassCount,
2260
- processUntilListEnd,
2261
- detailLimit: normalizedDetailLimit,
2262
- detailSource: normalizedDetailSource,
2263
- closeResume,
2264
- requestResumeForPassed,
2265
- dryRunRequestCv,
2266
- greetingText,
2267
- delayMs,
2268
- cardTimeoutMs,
2269
- readyTimeoutMs,
2270
- onlineResumeButtonTimeoutMs,
2271
- resumeDomTimeoutMs,
2272
- maxImagePages,
2273
- imageWheelDeltaY,
2274
- cvAcquisitionMode,
2275
- callLlmOnImage,
2276
- llmConfig,
2277
- llmTimeoutMs,
2278
- llmImageLimit,
2279
- llmImageDetail,
2280
- screeningMode: normalizedScreeningMode,
2281
- listMaxScrolls,
2282
- listStableSignatureLimit,
2283
- listWheelDeltaY,
2284
- listSettleMs,
2285
- listFallbackPoint,
2286
- imageOutputDir,
2287
- humanRestEnabled: effectiveHumanRestEnabled,
2288
- humanBehavior: effectiveHumanBehavior
2289
- }, runControl);
2290
- } catch (error) {
2291
- if (error instanceof RunCanceledError) throw error;
2292
- const finalFailureArtifact = await captureChatFinalFailureArtifact(client, {
2293
- runControl,
2294
- imageOutputDir,
2295
- error
2296
- });
2297
- if (finalFailureArtifact) {
2298
- runControl.checkpoint({
2299
- final_failure_artifact: finalFailureArtifact
2300
- });
2301
- }
2302
- throw error;
2303
- }
2304
- }
2305
- });
2306
- }
2279
+ checkpoint: {},
2280
+ task: async (runControl) => {
2281
+ try {
2282
+ return await workflow({
2283
+ client,
2284
+ targetUrl,
2285
+ job,
2286
+ startFrom,
2287
+ criteria,
2288
+ maxCandidates,
2289
+ targetPassCount,
2290
+ processUntilListEnd,
2291
+ detailLimit: normalizedDetailLimit,
2292
+ detailSource: normalizedDetailSource,
2293
+ closeResume,
2294
+ requestResumeForPassed,
2295
+ dryRunRequestCv,
2296
+ greetingText,
2297
+ delayMs,
2298
+ cardTimeoutMs,
2299
+ readyTimeoutMs,
2300
+ onlineResumeButtonTimeoutMs,
2301
+ resumeDomTimeoutMs,
2302
+ maxImagePages,
2303
+ imageWheelDeltaY,
2304
+ cvAcquisitionMode,
2305
+ callLlmOnImage,
2306
+ llmConfig,
2307
+ llmTimeoutMs,
2308
+ llmImageLimit,
2309
+ llmImageDetail,
2310
+ screeningMode: normalizedScreeningMode,
2311
+ listMaxScrolls,
2312
+ listStableSignatureLimit,
2313
+ listWheelDeltaY,
2314
+ listSettleMs,
2315
+ listFallbackPoint,
2316
+ imageOutputDir,
2317
+ humanRestEnabled: effectiveHumanRestEnabled,
2318
+ humanBehavior: effectiveHumanBehavior
2319
+ }, runControl);
2320
+ } catch (error) {
2321
+ if (error instanceof RunCanceledError) throw error;
2322
+ const finalFailureArtifact = await captureChatFinalFailureArtifact(client, {
2323
+ runControl,
2324
+ imageOutputDir,
2325
+ error
2326
+ });
2327
+ if (finalFailureArtifact) {
2328
+ runControl.checkpoint({
2329
+ final_failure_artifact: finalFailureArtifact
2330
+ });
2331
+ }
2332
+ throw error;
2333
+ }
2334
+ }
2335
+ });
2336
+ }
2307
2337
 
2308
2338
  return {
2309
2339
  startChatRun,