@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.15

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.
@@ -1,4 +1,7 @@
1
- import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
1
+ import {
2
+ captureScrolledNodeScreenshots,
3
+ captureViewportScreenshot
4
+ } from "../../core/capture/index.js";
2
5
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
3
6
  import {
4
7
  clickPoint,
@@ -32,7 +35,10 @@ import {
32
35
  resolveInfiniteListFallbackPoint
33
36
  } from "../../core/infinite-list/index.js";
34
37
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
35
- import { createRunLifecycleManager } from "../../core/run/index.js";
38
+ import {
39
+ createRunLifecycleManager,
40
+ RunCanceledError
41
+ } from "../../core/run/index.js";
36
42
  import {
37
43
  addTiming,
38
44
  imageEvidenceFilePath,
@@ -61,12 +67,14 @@ import {
61
67
  closeChatBlockingPanels,
62
68
  closeChatResumeModal,
63
69
  createChatProfileNetworkRecorder,
64
- extractChatProfileCandidate,
65
- isUnsafeChatOnlineResumeLinkError,
66
- openChatOnlineResume,
67
- quickChatResumeModalOpenProbe,
68
- readChatConversationReadyState,
69
- requestChatResumeForPassedCandidate,
70
+ extractChatProfileCandidate,
71
+ isChatOnlineResumeModalOpenFailureError,
72
+ isUnsafeChatOnlineResumeLinkError,
73
+ openChatOnlineResume,
74
+ quickChatResumeModalOpenProbe,
75
+ readChatActiveCandidateState,
76
+ readChatConversationReadyState,
77
+ requestChatResumeForPassedCandidate,
70
78
  selectChatMessageFilter,
71
79
  selectChatPrimaryLabel,
72
80
  waitForChatOnlineResumeButton,
@@ -373,12 +381,100 @@ function createFailedLlmResult(error) {
373
381
  };
374
382
  }
375
383
 
376
- function normalizeScreeningMode(value) {
377
- const normalized = String(value || "llm").trim().toLowerCase();
378
- return ["deterministic", "local", "local_scorer"].includes(normalized)
379
- ? "deterministic"
380
- : "llm";
381
- }
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
+ }
382
478
 
383
479
  function createMissingLlmConfigResult() {
384
480
  return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
@@ -400,17 +496,81 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
400
496
  };
401
497
  }
402
498
 
403
- function compactChatRuntimeError(error) {
404
- if (!error) return null;
405
- return {
406
- name: error.name || "Error",
407
- code: error.code || null,
408
- message: error.message || String(error),
409
- close_result: error.close_result || null,
410
- selection_ready_state: error.selection_ready_state || null,
411
- page_state: error.page_state || null
412
- };
413
- }
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
+ }
414
574
 
415
575
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
416
576
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
@@ -578,14 +738,15 @@ async function resolveFreshChatCardNodeId(client, {
578
738
  return freshNodeId || fallbackNodeId;
579
739
  }
580
740
 
581
- async function selectFreshChatCandidate(client, {
582
- cardNodeId,
583
- candidate,
584
- timeoutMs,
585
- settleMs = 1200
586
- } = {}) {
587
- let lastError = null;
588
- for (let attempt = 0; attempt < 3; attempt += 1) {
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) {
589
750
  const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
590
751
  const rootState = await getChatRoots(client);
591
752
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
@@ -596,16 +757,18 @@ async function selectFreshChatCandidate(client, {
596
757
  try {
597
758
  await scrollNodeIntoView(client, freshNodeId);
598
759
  await sleep(250);
599
- const box = await getNodeBox(client, freshNodeId);
600
- await clickPoint(client, box.center.x, box.center.y);
601
- if (settleMs > 0) await sleep(settleMs);
602
- const ready = await waitForChatOnlineResumeButton(client, {
603
- timeoutMs,
604
- expectedCandidateId: candidate?.id || ""
605
- });
606
- return {
607
- card_box: box,
608
- ready,
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,
609
772
  card_node_id: freshNodeId,
610
773
  refreshed_node: freshNodeId !== cardNodeId,
611
774
  modal_guard: modalGuard,
@@ -617,8 +780,35 @@ async function selectFreshChatCandidate(client, {
617
780
  await sleep(350);
618
781
  }
619
782
  }
620
- throw lastError || new Error("Chat candidate selection failed");
621
- }
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
+ }
622
812
 
623
813
  function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
624
814
  if (detailSource !== "network" && detailSource !== "cascade") return [];
@@ -760,15 +950,16 @@ export async function runChatWorkflow({
760
950
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
761
951
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
762
952
  });
763
- const humanRestController = createHumanRestController({
764
- enabled: effectiveHumanRestEnabled,
765
- shortRestEnabled: effectiveHumanBehavior.shortRest,
766
- batchRestEnabled: effectiveHumanBehavior.batchRest,
767
- restLevel: effectiveHumanBehavior.restLevel
768
- });
769
- const normalizedDetailSource = normalizeDetailSource(detailSource);
770
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
771
- const useLlmScreening = normalizedScreeningMode !== "deterministic";
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;
772
963
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
773
964
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
774
965
  ? Number(targetPassCount)
@@ -1214,11 +1405,12 @@ export async function runChatWorkflow({
1214
1405
  detailStep = "select_candidate";
1215
1406
  networkRecorder.clear();
1216
1407
  await maybeHumanActionCooldown("before_detail_open", timings);
1217
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1218
- cardNodeId,
1219
- candidate: cardCandidate,
1220
- timeoutMs: onlineResumeButtonTimeoutMs
1221
- }));
1408
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1409
+ cardNodeId,
1410
+ candidate: cardCandidate,
1411
+ timeoutMs: onlineResumeButtonTimeoutMs,
1412
+ onlineResumeProbe: !collectCvOnly
1413
+ }));
1222
1414
  if (selected.ready?.forbidden_top_level_navigation) {
1223
1415
  throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1224
1416
  }
@@ -1238,11 +1430,11 @@ export async function runChatWorkflow({
1238
1430
  detailResult.cv_acquisition.pre_detail_state = preActionState;
1239
1431
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
1240
1432
  }
1241
- if (!selected.ready?.ok) {
1242
- if (detailResult) {
1243
- // Already classified by the pre-detail conversation state.
1244
- } else if (selected.ready?.reason === "active_candidate_mismatch") {
1245
- throw makeChatCandidateSelectionMismatchError(selected, cardCandidate);
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);
1246
1438
  } else {
1247
1439
  detailStep = "read_conversation_ready_state";
1248
1440
  if (preActionState.attachment_resume_enabled) {
@@ -1252,13 +1444,18 @@ export async function runChatWorkflow({
1252
1444
  } else {
1253
1445
  detailUnavailableReason = "online_resume_button_unavailable";
1254
1446
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1255
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1256
- }
1257
- }
1258
- }
1259
-
1260
- if (!detailResult) {
1261
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
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);
1262
1459
  let networkWait = null;
1263
1460
  let contentWait = {
1264
1461
  ok: false,
@@ -1688,11 +1885,11 @@ export async function runChatWorkflow({
1688
1885
  recovery
1689
1886
  });
1690
1887
  continue;
1691
- } else if (isChatCandidateSelectionMismatchError(error)) {
1692
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1693
- if (retryCount < 1) {
1694
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1695
- const recovery = await recoverAndReapplyChatContext(
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(
1696
1893
  "active_candidate_mismatch",
1697
1894
  error,
1698
1895
  { forceRefresh: true }
@@ -1704,15 +1901,35 @@ export async function runChatWorkflow({
1704
1901
  continue;
1705
1902
  }
1706
1903
  detailUnavailableReason = "active_candidate_mismatch";
1707
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1708
- detailResult.cv_acquisition.selection_ready_state = error.selection_ready_state || null;
1709
- detailResult.cv_acquisition.recovery_attempted = true;
1710
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1711
- } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1712
- detailUnavailableReason = "unsafe_online_resume_navigation_link";
1713
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1714
- detailResult.cv_acquisition.blocked_pre_click = true;
1715
- detailResult.cv_acquisition.button_href = error.href || null;
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;
1716
1933
  detailResult.cv_acquisition.button_selector = error.button_selector || null;
1717
1934
  detailResult.cv_acquisition.attempts = error.attempts || null;
1718
1935
  } else {
@@ -1723,37 +1940,43 @@ export async function runChatWorkflow({
1723
1940
  await closeChatBlockingPanels(client, { attemptsLimit: 2 });
1724
1941
  }
1725
1942
  }
1726
- screeningCandidate = detailResult.candidate;
1727
- }
1943
+ screeningCandidate = detailResult?.candidate || cardCandidate;
1944
+ }
1728
1945
 
1729
1946
  await runControl.waitIfPaused();
1730
1947
  runControl.throwIfCanceled();
1731
1948
  runControl.setPhase("chat:screening");
1732
1949
  let cardOnlyLlmResult = null;
1733
- if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1734
- detailUnavailableReason = detailResult
1735
- ? "full_cv_not_acquired"
1736
- : "detail_not_opened_full_cv_required";
1737
- }
1738
- const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1739
- const screening = detailUnavailableReason
1740
- ? {
1741
- status: "skip",
1742
- passed: false,
1743
- score: 0,
1744
- reasons: [detailUnavailableReason],
1745
- candidate: screeningCandidate
1746
- }
1747
- : useLlmScreening
1748
- ? llmToScreening(effectiveLlmResult, screeningCandidate)
1749
- : screenCandidate(screeningCandidate, { criteria });
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 });
1750
1973
  let postAction = null;
1751
1974
  if (requestResumeForPassed && screening.passed) {
1752
1975
  await maybeHumanActionCooldown("before_post_action", timings);
1753
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1754
- greetingText,
1755
- dryRun: dryRunRequestCv
1756
- }));
1976
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1977
+ greetingText,
1978
+ dryRun: dryRunRequestCv
1979
+ }));
1757
1980
  if (postAction?.requested) requestSatisfiedCount += 1;
1758
1981
  if (postAction?.skipped) requestSkippedCount += 1;
1759
1982
  if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
@@ -1950,8 +2173,8 @@ export function createChatRunService({
1950
2173
  name = "chat-domain-run"
1951
2174
  } = {}) {
1952
2175
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
1953
- const normalizedDetailSource = normalizeDetailSource(detailSource);
1954
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
2176
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
2177
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
1955
2178
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1956
2179
  const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
1957
2180
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -1977,9 +2200,10 @@ export function createChatRunService({
1977
2200
  dry_run_request_cv: Boolean(dryRunRequestCv),
1978
2201
  greeting_text: greetingText,
1979
2202
  cv_acquisition_mode: cvAcquisitionMode,
1980
- call_llm_on_image: Boolean(callLlmOnImage),
1981
- screening_mode: normalizedScreeningMode,
1982
- llm_configured: Boolean(llmConfig),
2203
+ call_llm_on_image: Boolean(callLlmOnImage),
2204
+ screening_mode: normalizedScreeningMode,
2205
+ cv_collection_mode: normalizedScreeningMode === "collect_cv",
2206
+ llm_configured: Boolean(llmConfig),
1983
2207
  llm_timeout_ms: llmTimeoutMs,
1984
2208
  llm_image_limit: llmImageLimit,
1985
2209
  llm_image_detail: llmImageDetail,
@@ -1994,10 +2218,11 @@ export function createChatRunService({
1994
2218
  image_output_dir: imageOutputDir || "",
1995
2219
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1996
2220
  human_behavior_profile: effectiveHumanBehavior.profile,
1997
- human_behavior: effectiveHumanBehavior,
1998
- human_rest_level: effectiveHumanBehavior.restLevel,
1999
- human_rest_enabled: effectiveHumanRestEnabled
2000
- },
2221
+ human_behavior: effectiveHumanBehavior,
2222
+ human_rest_level: effectiveHumanBehavior.restLevel,
2223
+ human_rest_enabled: effectiveHumanRestEnabled,
2224
+ cv_collection_mode: normalizedScreeningMode === "collect_cv"
2225
+ },
2001
2226
  progress: {
2002
2227
  card_count: 0,
2003
2228
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -2021,47 +2246,64 @@ export function createChatRunService({
2021
2246
  human_rest_ms: 0,
2022
2247
  last_human_event: null
2023
2248
  },
2024
- checkpoint: {},
2025
- task: (runControl) => workflow({
2026
- client,
2027
- targetUrl,
2028
- job,
2029
- startFrom,
2030
- criteria,
2031
- maxCandidates,
2032
- targetPassCount,
2033
- processUntilListEnd,
2034
- detailLimit: normalizedDetailLimit,
2035
- detailSource: normalizedDetailSource,
2036
- closeResume,
2037
- requestResumeForPassed,
2038
- dryRunRequestCv,
2039
- greetingText,
2040
- delayMs,
2041
- cardTimeoutMs,
2042
- readyTimeoutMs,
2043
- onlineResumeButtonTimeoutMs,
2044
- resumeDomTimeoutMs,
2045
- maxImagePages,
2046
- imageWheelDeltaY,
2047
- cvAcquisitionMode,
2048
- callLlmOnImage,
2049
- llmConfig,
2050
- llmTimeoutMs,
2051
- llmImageLimit,
2052
- llmImageDetail,
2053
- screeningMode: normalizedScreeningMode,
2054
- listMaxScrolls,
2055
- listStableSignatureLimit,
2056
- listWheelDeltaY,
2057
- listSettleMs,
2058
- listFallbackPoint,
2059
- imageOutputDir,
2060
- humanRestEnabled: effectiveHumanRestEnabled,
2061
- humanBehavior: effectiveHumanBehavior
2062
- }, runControl)
2063
- });
2064
- }
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
+ }
2065
2307
 
2066
2308
  return {
2067
2309
  startChatRun,