@reconcrap/boss-recommend-mcp 2.1.21 → 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 -2415
  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 +571 -571
  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,
@@ -88,13 +88,13 @@ import {
88
88
  makeForbiddenChatResumeNavigationError,
89
89
  recoverChatShell
90
90
  } from "./page-guard.js";
91
- import { getChatRoots } from "./roots.js";
92
-
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;
96
-
97
- function normalizeDetailSource(value) {
91
+ import { getChatRoots } from "./roots.js";
92
+
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;
96
+
97
+ function normalizeDetailSource(value) {
98
98
  const normalized = String(value || "").trim().toLowerCase();
99
99
  return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
100
100
  }
@@ -116,35 +116,35 @@ function compactScreening(screening) {
116
116
 
117
117
  function compactLlmResult(llmResult) {
118
118
  if (!llmResult) return 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,
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,
129
129
  finish_reason: llmResult.finish_reason || null,
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
- }
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
+ }
148
148
 
149
149
  function compactCandidate(candidate) {
150
150
  return {
@@ -363,9 +363,9 @@ function isRecoverableLlmScreeningError(error) {
363
363
  }
364
364
 
365
365
  function createFailedLlmResult(error) {
366
- return {
367
- ok: false,
368
- passed: false,
366
+ return {
367
+ ok: false,
368
+ passed: false,
369
369
  reason: "",
370
370
  evidence: [],
371
371
  cot: "",
@@ -373,110 +373,110 @@ function createFailedLlmResult(error) {
373
373
  reasoning_content: "",
374
374
  raw_model_output: "",
375
375
  attempt_count: Number(error?.llm_attempt_count) || 0,
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
- }
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
+ }
480
480
 
481
481
  function createMissingLlmConfigResult() {
482
482
  return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
@@ -498,81 +498,81 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
498
498
  };
499
499
  }
500
500
 
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
- }
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
+ }
576
576
 
577
577
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
578
578
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
@@ -740,15 +740,15 @@ async function resolveFreshChatCardNodeId(client, {
740
740
  return freshNodeId || fallbackNodeId;
741
741
  }
742
742
 
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) {
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) {
752
752
  const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
753
753
  const rootState = await getChatRoots(client);
754
754
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
@@ -759,18 +759,18 @@ async function selectFreshChatCandidate(client, {
759
759
  try {
760
760
  await scrollNodeIntoView(client, freshNodeId);
761
761
  await sleep(250);
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,
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,
774
774
  card_node_id: freshNodeId,
775
775
  refreshed_node: freshNodeId !== cardNodeId,
776
776
  modal_guard: modalGuard,
@@ -782,35 +782,35 @@ async function selectFreshChatCandidate(client, {
782
782
  await sleep(350);
783
783
  }
784
784
  }
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
- }
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
+ }
814
814
 
815
815
  function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
816
816
  if (detailSource !== "network" && detailSource !== "cascade") return [];
@@ -941,32 +941,32 @@ export async function runChatWorkflow({
941
941
  humanBehavior = null
942
942
  } = {}, runControl) {
943
943
  if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
944
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
945
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
946
- });
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;
953
- configureHumanInteraction(client, {
954
- enabled: effectiveHumanBehavior.enabled,
955
- clickMovementEnabled: effectiveHumanBehavior.clickMovement,
944
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
945
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
946
+ });
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;
953
+ configureHumanInteraction(client, {
954
+ enabled: effectiveHumanBehavior.enabled,
955
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
956
956
  textEntryEnabled: effectiveHumanBehavior.textEntry,
957
957
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
958
958
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
959
959
  });
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
- });
969
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
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
+ });
969
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
970
970
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
971
971
  ? Number(targetPassCount)
972
972
  : null;
@@ -1006,12 +1006,12 @@ export async function runChatWorkflow({
1006
1006
  let requestedCount = 0;
1007
1007
  let requestSatisfiedCount = 0;
1008
1008
  let requestSkippedCount = 0;
1009
- let contextSetup = {};
1010
- let contextRecoveryAttempts = 0;
1011
- const candidateRecoveryCounts = new Map();
1012
- let lastHumanEvent = null;
1013
-
1014
- 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) {
1015
1015
  if (!event) return lastHumanEvent;
1016
1016
  lastHumanEvent = {
1017
1017
  at: new Date().toISOString(),
@@ -1075,13 +1075,13 @@ export async function runChatWorkflow({
1075
1075
  ...setup.contextSetup,
1076
1076
  initial_top_level_state: initialTopLevelState
1077
1077
  };
1078
- runControl.checkpoint({
1079
- chat_context: contextSetup
1080
- });
1081
-
1082
- async function recoverAndReapplyChatContext(reason, error = null, {
1083
- forceRefresh = false
1084
- } = {}) {
1078
+ runControl.checkpoint({
1079
+ chat_context: contextSetup
1080
+ });
1081
+
1082
+ async function recoverAndReapplyChatContext(reason, error = null, {
1083
+ forceRefresh = false
1084
+ } = {}) {
1085
1085
  runControl.setPhase("chat:recover_shell");
1086
1086
  contextRecoveryAttempts += 1;
1087
1087
  const shellRecovery = await recoverChatShell(client, {
@@ -1183,14 +1183,14 @@ export async function runChatWorkflow({
1183
1183
  viewport_checks: viewportGuard.getStats().checks,
1184
1184
  viewport_recoveries: viewportGuard.getStats().recoveries,
1185
1185
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1186
- human_behavior_profile: effectiveHumanBehavior.profile,
1187
- human_rest_level: effectiveHumanBehavior.restLevel,
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,
1192
- human_rest_count: humanRestController.getState().rest_count,
1193
- human_rest_ms: humanRestController.getState().total_rest_ms,
1186
+ human_behavior_profile: effectiveHumanBehavior.profile,
1187
+ human_rest_level: effectiveHumanBehavior.restLevel,
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,
1192
+ human_rest_count: humanRestController.getState().rest_count,
1193
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1194
1194
  last_human_event: lastHumanEvent
1195
1195
  });
1196
1196
  runControl.setPhase("chat:done");
@@ -1214,10 +1214,10 @@ export async function runChatWorkflow({
1214
1214
  last_human_event: lastHumanEvent,
1215
1215
  list_end_reason: listEndReason,
1216
1216
  target_pass_count: passTarget,
1217
- process_until_list_end: Boolean(processUntilListEnd),
1218
- processed_limit: processedLimit,
1219
- detail_source: normalizedDetailSource,
1220
- processed: 0,
1217
+ process_until_list_end: Boolean(processUntilListEnd),
1218
+ processed_limit: processedLimit,
1219
+ detail_source: normalizedDetailSource,
1220
+ processed: 0,
1221
1221
  screened: 0,
1222
1222
  detail_opened: 0,
1223
1223
  llm_screened: 0,
@@ -1248,20 +1248,20 @@ export async function runChatWorkflow({
1248
1248
  screening_mode: normalizedScreeningMode,
1249
1249
  unique_seen: compactInfiniteListState(listState).seen_count,
1250
1250
  scroll_count: 0,
1251
- context_recoveries: contextRecoveryAttempts,
1252
- viewport_checks: viewportGuard.getStats().checks,
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
1264
- });
1251
+ context_recoveries: contextRecoveryAttempts,
1252
+ viewport_checks: viewportGuard.getStats().checks,
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
1264
+ });
1265
1265
 
1266
1266
  while (
1267
1267
  results.length < processedLimit
@@ -1424,12 +1424,12 @@ export async function runChatWorkflow({
1424
1424
  detailStep = "select_candidate";
1425
1425
  networkRecorder.clear();
1426
1426
  await maybeHumanActionCooldown("before_detail_open", timings);
1427
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1428
- cardNodeId,
1429
- candidate: cardCandidate,
1430
- timeoutMs: onlineResumeButtonTimeoutMs,
1431
- onlineResumeProbe: !collectCvOnly
1432
- }));
1427
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1428
+ cardNodeId,
1429
+ candidate: cardCandidate,
1430
+ timeoutMs: onlineResumeButtonTimeoutMs,
1431
+ onlineResumeProbe: !collectCvOnly
1432
+ }));
1433
1433
  if (selected.ready?.forbidden_top_level_navigation) {
1434
1434
  throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1435
1435
  }
@@ -1449,11 +1449,11 @@ export async function runChatWorkflow({
1449
1449
  detailResult.cv_acquisition.pre_detail_state = preActionState;
1450
1450
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
1451
1451
  }
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);
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);
1457
1457
  } else {
1458
1458
  detailStep = "read_conversation_ready_state";
1459
1459
  if (preActionState.attachment_resume_enabled) {
@@ -1463,18 +1463,18 @@ export async function runChatWorkflow({
1463
1463
  } else {
1464
1464
  detailUnavailableReason = "online_resume_button_unavailable";
1465
1465
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
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);
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);
1478
1478
  let networkWait = null;
1479
1479
  let contentWait = {
1480
1480
  ok: false,
@@ -1734,24 +1734,24 @@ export async function runChatWorkflow({
1734
1734
  llmResult = createMissingLlmConfigResult();
1735
1735
  } else {
1736
1736
  try {
1737
- llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1738
- candidate: detailResult.candidate,
1739
- criteria,
1740
- config: llmConfig,
1741
- timeoutMs: llmTimeoutMs,
1737
+ llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1738
+ candidate: detailResult.candidate,
1739
+ criteria,
1740
+ config: llmConfig,
1741
+ timeoutMs: llmTimeoutMs,
1742
1742
  imageEvidence,
1743
1743
  maxImages: llmImageLimit,
1744
1744
  imageDetail: llmImageDetail
1745
1745
  }));
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
- }
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
+ }
1755
1755
  }
1756
1756
  }
1757
1757
  } else {
@@ -1813,24 +1813,24 @@ export async function runChatWorkflow({
1813
1813
  const llmTimingKey = imageEvidence?.file_paths?.length
1814
1814
  ? "vision_model_ms"
1815
1815
  : "text_model_ms";
1816
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1817
- candidate: detailResult.candidate,
1818
- criteria,
1819
- config: llmConfig,
1816
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1817
+ candidate: detailResult.candidate,
1818
+ criteria,
1819
+ config: llmConfig,
1820
1820
  timeoutMs: llmTimeoutMs,
1821
1821
  imageEvidence,
1822
1822
  maxImages: llmImageLimit,
1823
1823
  imageDetail: llmImageDetail
1824
1824
  }));
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
- }
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
+ }
1834
1834
  }
1835
1835
  }
1836
1836
  }
@@ -1904,11 +1904,11 @@ export async function runChatWorkflow({
1904
1904
  recovery
1905
1905
  });
1906
1906
  continue;
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(
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(
1912
1912
  "active_candidate_mismatch",
1913
1913
  error,
1914
1914
  { forceRefresh: true }
@@ -1920,35 +1920,35 @@ export async function runChatWorkflow({
1920
1920
  continue;
1921
1921
  }
1922
1922
  detailUnavailableReason = "active_candidate_mismatch";
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;
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;
1952
1952
  detailResult.cv_acquisition.button_selector = error.button_selector || null;
1953
1953
  detailResult.cv_acquisition.attempts = error.attempts || null;
1954
1954
  } else {
@@ -1959,43 +1959,43 @@ export async function runChatWorkflow({
1959
1959
  await closeChatBlockingPanels(client, { attemptsLimit: 2 });
1960
1960
  }
1961
1961
  }
1962
- screeningCandidate = detailResult?.candidate || cardCandidate;
1963
- }
1962
+ screeningCandidate = detailResult?.candidate || cardCandidate;
1963
+ }
1964
1964
 
1965
1965
  await runControl.waitIfPaused();
1966
1966
  runControl.throwIfCanceled();
1967
1967
  runControl.setPhase("chat:screening");
1968
1968
  let cardOnlyLlmResult = null;
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 });
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 });
1992
1992
  let postAction = null;
1993
1993
  if (requestResumeForPassed && screening.passed) {
1994
1994
  await maybeHumanActionCooldown("before_post_action", timings);
1995
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1996
- greetingText,
1997
- dryRun: dryRunRequestCv
1998
- }));
1995
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1996
+ greetingText,
1997
+ dryRun: dryRunRequestCv
1998
+ }));
1999
1999
  if (postAction?.requested) requestSatisfiedCount += 1;
2000
2000
  if (postAction?.skipped) requestSkippedCount += 1;
2001
2001
  if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
@@ -2046,14 +2046,14 @@ export async function runChatWorkflow({
2046
2046
  viewport_checks: viewportGuard.getStats().checks,
2047
2047
  viewport_recoveries: viewportGuard.getStats().recoveries,
2048
2048
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2049
- human_behavior_profile: effectiveHumanBehavior.profile,
2050
- human_rest_level: effectiveHumanBehavior.restLevel,
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,
2055
- human_rest_count: humanRestController.getState().rest_count,
2056
- human_rest_ms: humanRestController.getState().total_rest_ms,
2049
+ human_behavior_profile: effectiveHumanBehavior.profile,
2050
+ human_rest_level: effectiveHumanBehavior.restLevel,
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,
2055
+ human_rest_count: humanRestController.getState().rest_count,
2056
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2057
2057
  last_human_event: lastHumanEvent,
2058
2058
  last_candidate_id: screeningCandidate.id || null,
2059
2059
  last_candidate_key: candidateKey,
@@ -2093,11 +2093,11 @@ export async function runChatWorkflow({
2093
2093
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
2094
2094
  compactResult.timings.total_ms = Date.now() - candidateStarted;
2095
2095
  runControl.updateProgress({
2096
- human_rest_enabled: effectiveHumanRestEnabled,
2097
- human_rest_level: effectiveHumanBehavior.restLevel,
2098
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2099
- human_rest_count: humanRestController.getState().rest_count,
2100
- human_rest_ms: humanRestController.getState().total_rest_ms,
2096
+ human_rest_enabled: effectiveHumanRestEnabled,
2097
+ human_rest_level: effectiveHumanBehavior.restLevel,
2098
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2099
+ human_rest_count: humanRestController.getState().rest_count,
2100
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2101
2101
  human_rest_last: restResult,
2102
2102
  context_recoveries: contextRecoveryAttempts,
2103
2103
  last_human_event: lastHumanEvent
@@ -2130,10 +2130,10 @@ export async function runChatWorkflow({
2130
2130
  last_human_event: lastHumanEvent,
2131
2131
  list_end_reason: listEndReason || null,
2132
2132
  target_pass_count: passTarget,
2133
- process_until_list_end: Boolean(processUntilListEnd),
2134
- processed_limit: processedLimit,
2135
- detail_source: normalizedDetailSource,
2136
- processed: finalCounters.processed,
2133
+ process_until_list_end: Boolean(processUntilListEnd),
2134
+ processed_limit: processedLimit,
2135
+ detail_source: normalizedDetailSource,
2136
+ processed: finalCounters.processed,
2137
2137
  screened: finalCounters.screened,
2138
2138
  detail_opened: finalCounters.detail_opened,
2139
2139
  llm_screened: finalCounters.llm_screened,
@@ -2196,16 +2196,16 @@ export function createChatRunService({
2196
2196
  name = "chat-domain-run"
2197
2197
  } = {}) {
2198
2198
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
2199
- const normalizedDetailSource = normalizeDetailSource(detailSource);
2200
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2201
- const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
2202
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2203
- const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2204
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2205
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2206
- });
2207
- const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
2208
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
2199
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
2200
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2201
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
2202
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2203
+ const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2204
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2205
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2206
+ });
2207
+ const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
2208
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
2209
2209
  return manager.startRun({
2210
2210
  runId,
2211
2211
  name,
@@ -2225,16 +2225,16 @@ export function createChatRunService({
2225
2225
  dry_run_request_cv: Boolean(dryRunRequestCv),
2226
2226
  greeting_text: greetingText,
2227
2227
  cv_acquisition_mode: cvAcquisitionMode,
2228
- call_llm_on_image: Boolean(callLlmOnImage),
2229
- screening_mode: normalizedScreeningMode,
2230
- cv_collection_mode: normalizedScreeningMode === "collect_cv",
2231
- 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),
2232
2232
  llm_timeout_ms: llmTimeoutMs,
2233
2233
  llm_image_limit: llmImageLimit,
2234
- llm_image_detail: llmImageDetail,
2235
- max_image_pages: maxImagePages,
2236
- image_wheel_delta_y: imageWheelDeltaY,
2237
- list_max_scrolls: listMaxScrolls,
2234
+ llm_image_detail: llmImageDetail,
2235
+ max_image_pages: maxImagePages,
2236
+ image_wheel_delta_y: imageWheelDeltaY,
2237
+ list_max_scrolls: listMaxScrolls,
2238
2238
  list_stable_signature_limit: listStableSignatureLimit,
2239
2239
  list_wheel_delta_y: listWheelDeltaY,
2240
2240
  list_settle_ms: listSettleMs,
@@ -2243,13 +2243,13 @@ export function createChatRunService({
2243
2243
  image_output_dir: imageOutputDir || "",
2244
2244
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2245
2245
  human_behavior_profile: effectiveHumanBehavior.profile,
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
- },
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
+ },
2253
2253
  progress: {
2254
2254
  card_count: 0,
2255
2255
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -2266,74 +2266,74 @@ export function createChatRunService({
2266
2266
  request_skipped: 0,
2267
2267
  context_recoveries: 0,
2268
2268
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2269
- human_behavior_profile: effectiveHumanBehavior.profile,
2270
- human_rest_level: effectiveHumanBehavior.restLevel,
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,
2275
- human_rest_count: 0,
2269
+ human_behavior_profile: effectiveHumanBehavior.profile,
2270
+ human_rest_level: effectiveHumanBehavior.restLevel,
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,
2275
+ human_rest_count: 0,
2276
2276
  human_rest_ms: 0,
2277
2277
  last_human_event: null
2278
2278
  },
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
- }
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
+ }
2337
2337
 
2338
2338
  return {
2339
2339
  startChatRun,