@reconcrap/boss-recommend-mcp 2.1.13 → 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,
@@ -40,6 +46,8 @@ import {
40
46
  } from "../../core/run/timing.js";
41
47
  import {
42
48
  callScreeningLlm,
49
+ createFatalLlmRunError,
50
+ isFatalLlmProviderError,
43
51
  normalizeText,
44
52
  screenCandidate
45
53
  } from "../../core/screening/index.js";
@@ -59,12 +67,14 @@ import {
59
67
  closeChatBlockingPanels,
60
68
  closeChatResumeModal,
61
69
  createChatProfileNetworkRecorder,
62
- extractChatProfileCandidate,
63
- isUnsafeChatOnlineResumeLinkError,
64
- openChatOnlineResume,
65
- quickChatResumeModalOpenProbe,
66
- readChatConversationReadyState,
67
- requestChatResumeForPassedCandidate,
70
+ extractChatProfileCandidate,
71
+ isChatOnlineResumeModalOpenFailureError,
72
+ isUnsafeChatOnlineResumeLinkError,
73
+ openChatOnlineResume,
74
+ quickChatResumeModalOpenProbe,
75
+ readChatActiveCandidateState,
76
+ readChatConversationReadyState,
77
+ requestChatResumeForPassedCandidate,
68
78
  selectChatMessageFilter,
69
79
  selectChatPrimaryLabel,
70
80
  waitForChatOnlineResumeButton,
@@ -108,19 +118,31 @@ function compactLlmResult(llmResult) {
108
118
  ok: Boolean(llmResult.ok),
109
119
  provider: llmResult.provider || null,
110
120
  passed: llmResult.passed,
121
+ review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
111
122
  cot: llmResult.cot || llmResult.decision_cot || "",
112
- reasoning_content: llmResult.reasoning_content || "",
113
- raw_model_output: llmResult.raw_model_output || "",
114
- evidence_count: llmResult.evidence?.length || 0,
115
- usage: llmResult.usage || null,
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,
116
127
  finish_reason: llmResult.finish_reason || null,
117
- image_input_count: llmResult.image_input_count || 0,
118
- attempt_count: llmResult.attempt_count || 0,
119
- fallback_count: llmResult.fallback_count || 0,
120
- llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
121
- error: llmResult.error || null
122
- };
123
- }
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
+ }
124
146
 
125
147
  function compactCandidate(candidate) {
126
148
  return {
@@ -339,9 +361,9 @@ function isRecoverableLlmScreeningError(error) {
339
361
  }
340
362
 
341
363
  function createFailedLlmResult(error) {
342
- return {
343
- ok: false,
344
- passed: false,
364
+ return {
365
+ ok: false,
366
+ passed: false,
345
367
  reason: "",
346
368
  evidence: [],
347
369
  cot: "",
@@ -349,19 +371,110 @@ function createFailedLlmResult(error) {
349
371
  reasoning_content: "",
350
372
  raw_model_output: "",
351
373
  attempt_count: Number(error?.llm_attempt_count) || 0,
352
- fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
353
- llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
354
- error: error?.message || String(error || "unknown"),
355
- screened_at: new Date().toISOString()
356
- };
357
- }
358
-
359
- function normalizeScreeningMode(value) {
360
- const normalized = String(value || "llm").trim().toLowerCase();
361
- return ["deterministic", "local", "local_scorer"].includes(normalized)
362
- ? "deterministic"
363
- : "llm";
364
- }
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
+ }
365
478
 
366
479
  function createMissingLlmConfigResult() {
367
480
  return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
@@ -383,17 +496,81 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
383
496
  };
384
497
  }
385
498
 
386
- function compactChatRuntimeError(error) {
387
- if (!error) return null;
388
- return {
389
- name: error.name || "Error",
390
- code: error.code || null,
391
- message: error.message || String(error),
392
- close_result: error.close_result || null,
393
- selection_ready_state: error.selection_ready_state || null,
394
- page_state: error.page_state || null
395
- };
396
- }
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
+ }
397
574
 
398
575
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
399
576
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
@@ -561,14 +738,15 @@ async function resolveFreshChatCardNodeId(client, {
561
738
  return freshNodeId || fallbackNodeId;
562
739
  }
563
740
 
564
- async function selectFreshChatCandidate(client, {
565
- cardNodeId,
566
- candidate,
567
- timeoutMs,
568
- settleMs = 1200
569
- } = {}) {
570
- let lastError = null;
571
- 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) {
572
750
  const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
573
751
  const rootState = await getChatRoots(client);
574
752
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
@@ -579,16 +757,18 @@ async function selectFreshChatCandidate(client, {
579
757
  try {
580
758
  await scrollNodeIntoView(client, freshNodeId);
581
759
  await sleep(250);
582
- const box = await getNodeBox(client, freshNodeId);
583
- await clickPoint(client, box.center.x, box.center.y);
584
- if (settleMs > 0) await sleep(settleMs);
585
- const ready = await waitForChatOnlineResumeButton(client, {
586
- timeoutMs,
587
- expectedCandidateId: candidate?.id || ""
588
- });
589
- return {
590
- card_box: box,
591
- 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,
592
772
  card_node_id: freshNodeId,
593
773
  refreshed_node: freshNodeId !== cardNodeId,
594
774
  modal_guard: modalGuard,
@@ -600,8 +780,35 @@ async function selectFreshChatCandidate(client, {
600
780
  await sleep(350);
601
781
  }
602
782
  }
603
- throw lastError || new Error("Chat candidate selection failed");
604
- }
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
+ }
605
812
 
606
813
  function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
607
814
  if (detailSource !== "network" && detailSource !== "cascade") return [];
@@ -743,15 +950,16 @@ export async function runChatWorkflow({
743
950
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
744
951
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
745
952
  });
746
- const humanRestController = createHumanRestController({
747
- enabled: effectiveHumanRestEnabled,
748
- shortRestEnabled: effectiveHumanBehavior.shortRest,
749
- batchRestEnabled: effectiveHumanBehavior.batchRest,
750
- restLevel: effectiveHumanBehavior.restLevel
751
- });
752
- const normalizedDetailSource = normalizeDetailSource(detailSource);
753
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
754
- 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;
755
963
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
756
964
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
757
965
  ? Number(targetPassCount)
@@ -1197,11 +1405,12 @@ export async function runChatWorkflow({
1197
1405
  detailStep = "select_candidate";
1198
1406
  networkRecorder.clear();
1199
1407
  await maybeHumanActionCooldown("before_detail_open", timings);
1200
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1201
- cardNodeId,
1202
- candidate: cardCandidate,
1203
- timeoutMs: onlineResumeButtonTimeoutMs
1204
- }));
1408
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1409
+ cardNodeId,
1410
+ candidate: cardCandidate,
1411
+ timeoutMs: onlineResumeButtonTimeoutMs,
1412
+ onlineResumeProbe: !collectCvOnly
1413
+ }));
1205
1414
  if (selected.ready?.forbidden_top_level_navigation) {
1206
1415
  throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1207
1416
  }
@@ -1221,11 +1430,11 @@ export async function runChatWorkflow({
1221
1430
  detailResult.cv_acquisition.pre_detail_state = preActionState;
1222
1431
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
1223
1432
  }
1224
- if (!selected.ready?.ok) {
1225
- if (detailResult) {
1226
- // Already classified by the pre-detail conversation state.
1227
- } else if (selected.ready?.reason === "active_candidate_mismatch") {
1228
- 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);
1229
1438
  } else {
1230
1439
  detailStep = "read_conversation_ready_state";
1231
1440
  if (preActionState.attachment_resume_enabled) {
@@ -1235,13 +1444,18 @@ export async function runChatWorkflow({
1235
1444
  } else {
1236
1445
  detailUnavailableReason = "online_resume_button_unavailable";
1237
1446
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1238
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1239
- }
1240
- }
1241
- }
1242
-
1243
- if (!detailResult) {
1244
- 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);
1245
1459
  let networkWait = null;
1246
1460
  let contentWait = {
1247
1461
  ok: false,
@@ -1510,9 +1724,15 @@ export async function runChatWorkflow({
1510
1724
  maxImages: llmImageLimit,
1511
1725
  imageDetail: llmImageDetail
1512
1726
  }));
1513
- } catch (error) {
1514
- llmResult = createFailedLlmResult(error);
1515
- }
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
+ }
1516
1736
  }
1517
1737
  }
1518
1738
  } else {
@@ -1583,9 +1803,15 @@ export async function runChatWorkflow({
1583
1803
  maxImages: llmImageLimit,
1584
1804
  imageDetail: llmImageDetail
1585
1805
  }));
1586
- } catch (error) {
1587
- llmResult = createFailedLlmResult(error);
1588
- }
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
+ }
1589
1815
  }
1590
1816
  }
1591
1817
  }
@@ -1659,11 +1885,11 @@ export async function runChatWorkflow({
1659
1885
  recovery
1660
1886
  });
1661
1887
  continue;
1662
- } else if (isChatCandidateSelectionMismatchError(error)) {
1663
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1664
- if (retryCount < 1) {
1665
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1666
- 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(
1667
1893
  "active_candidate_mismatch",
1668
1894
  error,
1669
1895
  { forceRefresh: true }
@@ -1675,15 +1901,35 @@ export async function runChatWorkflow({
1675
1901
  continue;
1676
1902
  }
1677
1903
  detailUnavailableReason = "active_candidate_mismatch";
1678
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1679
- detailResult.cv_acquisition.selection_ready_state = error.selection_ready_state || null;
1680
- detailResult.cv_acquisition.recovery_attempted = true;
1681
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1682
- } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1683
- detailUnavailableReason = "unsafe_online_resume_navigation_link";
1684
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1685
- detailResult.cv_acquisition.blocked_pre_click = true;
1686
- 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;
1687
1933
  detailResult.cv_acquisition.button_selector = error.button_selector || null;
1688
1934
  detailResult.cv_acquisition.attempts = error.attempts || null;
1689
1935
  } else {
@@ -1694,37 +1940,43 @@ export async function runChatWorkflow({
1694
1940
  await closeChatBlockingPanels(client, { attemptsLimit: 2 });
1695
1941
  }
1696
1942
  }
1697
- screeningCandidate = detailResult.candidate;
1698
- }
1943
+ screeningCandidate = detailResult?.candidate || cardCandidate;
1944
+ }
1699
1945
 
1700
1946
  await runControl.waitIfPaused();
1701
1947
  runControl.throwIfCanceled();
1702
1948
  runControl.setPhase("chat:screening");
1703
1949
  let cardOnlyLlmResult = null;
1704
- if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1705
- detailUnavailableReason = detailResult
1706
- ? "full_cv_not_acquired"
1707
- : "detail_not_opened_full_cv_required";
1708
- }
1709
- const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1710
- const screening = detailUnavailableReason
1711
- ? {
1712
- status: "skip",
1713
- passed: false,
1714
- score: 0,
1715
- reasons: [detailUnavailableReason],
1716
- candidate: screeningCandidate
1717
- }
1718
- : useLlmScreening
1719
- ? llmToScreening(effectiveLlmResult, screeningCandidate)
1720
- : 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 });
1721
1973
  let postAction = null;
1722
1974
  if (requestResumeForPassed && screening.passed) {
1723
1975
  await maybeHumanActionCooldown("before_post_action", timings);
1724
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1725
- greetingText,
1726
- dryRun: dryRunRequestCv
1727
- }));
1976
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1977
+ greetingText,
1978
+ dryRun: dryRunRequestCv
1979
+ }));
1728
1980
  if (postAction?.requested) requestSatisfiedCount += 1;
1729
1981
  if (postAction?.skipped) requestSkippedCount += 1;
1730
1982
  if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
@@ -1921,8 +2173,8 @@ export function createChatRunService({
1921
2173
  name = "chat-domain-run"
1922
2174
  } = {}) {
1923
2175
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
1924
- const normalizedDetailSource = normalizeDetailSource(detailSource);
1925
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
2176
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
2177
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
1926
2178
  const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1927
2179
  const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
1928
2180
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -1948,9 +2200,10 @@ export function createChatRunService({
1948
2200
  dry_run_request_cv: Boolean(dryRunRequestCv),
1949
2201
  greeting_text: greetingText,
1950
2202
  cv_acquisition_mode: cvAcquisitionMode,
1951
- call_llm_on_image: Boolean(callLlmOnImage),
1952
- screening_mode: normalizedScreeningMode,
1953
- 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),
1954
2207
  llm_timeout_ms: llmTimeoutMs,
1955
2208
  llm_image_limit: llmImageLimit,
1956
2209
  llm_image_detail: llmImageDetail,
@@ -1965,10 +2218,11 @@ export function createChatRunService({
1965
2218
  image_output_dir: imageOutputDir || "",
1966
2219
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1967
2220
  human_behavior_profile: effectiveHumanBehavior.profile,
1968
- human_behavior: effectiveHumanBehavior,
1969
- human_rest_level: effectiveHumanBehavior.restLevel,
1970
- human_rest_enabled: effectiveHumanRestEnabled
1971
- },
2221
+ human_behavior: effectiveHumanBehavior,
2222
+ human_rest_level: effectiveHumanBehavior.restLevel,
2223
+ human_rest_enabled: effectiveHumanRestEnabled,
2224
+ cv_collection_mode: normalizedScreeningMode === "collect_cv"
2225
+ },
1972
2226
  progress: {
1973
2227
  card_count: 0,
1974
2228
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -1992,47 +2246,64 @@ export function createChatRunService({
1992
2246
  human_rest_ms: 0,
1993
2247
  last_human_event: null
1994
2248
  },
1995
- checkpoint: {},
1996
- task: (runControl) => workflow({
1997
- client,
1998
- targetUrl,
1999
- job,
2000
- startFrom,
2001
- criteria,
2002
- maxCandidates,
2003
- targetPassCount,
2004
- processUntilListEnd,
2005
- detailLimit: normalizedDetailLimit,
2006
- detailSource: normalizedDetailSource,
2007
- closeResume,
2008
- requestResumeForPassed,
2009
- dryRunRequestCv,
2010
- greetingText,
2011
- delayMs,
2012
- cardTimeoutMs,
2013
- readyTimeoutMs,
2014
- onlineResumeButtonTimeoutMs,
2015
- resumeDomTimeoutMs,
2016
- maxImagePages,
2017
- imageWheelDeltaY,
2018
- cvAcquisitionMode,
2019
- callLlmOnImage,
2020
- llmConfig,
2021
- llmTimeoutMs,
2022
- llmImageLimit,
2023
- llmImageDetail,
2024
- screeningMode: normalizedScreeningMode,
2025
- listMaxScrolls,
2026
- listStableSignatureLimit,
2027
- listWheelDeltaY,
2028
- listSettleMs,
2029
- listFallbackPoint,
2030
- imageOutputDir,
2031
- humanRestEnabled: effectiveHumanRestEnabled,
2032
- humanBehavior: effectiveHumanBehavior
2033
- }, runControl)
2034
- });
2035
- }
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
+ }
2036
2307
 
2037
2308
  return {
2038
2309
  startChatRun,