@reconcrap/boss-recommend-mcp 2.1.24 → 2.1.25
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.
- package/package.json +1 -1
- package/src/chat-runtime-config.js +7 -5
- package/src/core/browser/index.js +16 -9
- package/src/core/run/index.js +5 -4
- package/src/core/screening/index.js +291 -26
- package/src/domains/chat/action-journal.js +97 -4
- package/src/domains/recommend/actions.js +740 -36
- package/src/domains/recommend/colleague-contact.js +836 -90
- package/src/domains/recommend/detail.js +4029 -128
- package/src/domains/recommend/filters.js +46 -28
- package/src/domains/recommend/jobs.js +32 -3
- package/src/domains/recommend/location.js +93 -46
- package/src/domains/recommend/refresh.js +354 -69
- package/src/domains/recommend/run-service.js +2499 -484
- package/src/recommend-mcp.js +514 -92
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
4
6
|
import {
|
|
5
7
|
addTiming,
|
|
@@ -46,7 +48,8 @@ import {
|
|
|
46
48
|
resetInfiniteListForRefreshRound,
|
|
47
49
|
resolveInfiniteListFallbackPoint
|
|
48
50
|
} from "../../core/infinite-list/index.js";
|
|
49
|
-
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
51
|
+
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
52
|
+
import { createChatActionJournal } from "../chat/action-journal.js";
|
|
50
53
|
import {
|
|
51
54
|
callScreeningLlm,
|
|
52
55
|
compactScreeningLlmResult,
|
|
@@ -57,14 +60,20 @@ import {
|
|
|
57
60
|
screenCandidate
|
|
58
61
|
} from "../../core/screening/index.js";
|
|
59
62
|
import {
|
|
60
|
-
closeRecommendBlockingPanels,
|
|
61
|
-
closeRecommendAvatarPreview,
|
|
62
|
-
closeRecommendDetail,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
closeRecommendBlockingPanels,
|
|
64
|
+
closeRecommendAvatarPreview,
|
|
65
|
+
closeRecommendDetail,
|
|
66
|
+
compactRecommendDetailCandidateBinding,
|
|
67
|
+
createRecommendDetailCandidateBindingError,
|
|
68
|
+
createRecommendDetailNetworkRecorder,
|
|
69
|
+
extractRecommendDetailCandidate,
|
|
70
|
+
isRecommendDetailCandidateBindingError,
|
|
71
|
+
isRecommendDetailOpenMissError,
|
|
72
|
+
isRecommendPreClickStaleNoActionError,
|
|
73
|
+
isStaleRecommendNodeError,
|
|
67
74
|
openRecommendCardDetailWithFreshRetry,
|
|
75
|
+
verifyExactCardClickToNewResumeRootCausality,
|
|
76
|
+
verifyRecommendDetailCandidateBinding,
|
|
68
77
|
waitForRecommendDetail,
|
|
69
78
|
waitForRecommendDetailNetworkEvents
|
|
70
79
|
} from "./detail.js";
|
|
@@ -74,12 +83,15 @@ import {
|
|
|
74
83
|
} from "./cards.js";
|
|
75
84
|
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
76
85
|
import { ensureRecommendCurrentCityOnly } from "./location.js";
|
|
77
|
-
import {
|
|
78
|
-
applyRecommendFilterEnvelopeStages,
|
|
79
|
-
buildRecommendFilterSelectionOptions,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
import {
|
|
87
|
+
applyRecommendFilterEnvelopeStages,
|
|
88
|
+
buildRecommendFilterSelectionOptions,
|
|
89
|
+
inspectRecommendFilteredEmptyState,
|
|
90
|
+
isVerifiedRecommendFilterApplication,
|
|
91
|
+
isVerifiedRecommendRefreshExhaustion,
|
|
92
|
+
refreshRecommendListAtEnd,
|
|
93
|
+
selectRecommendJobWithRootRefresh
|
|
94
|
+
} from "./refresh.js";
|
|
83
95
|
import {
|
|
84
96
|
normalizeRecommendPageScope,
|
|
85
97
|
selectRecommendPageScope
|
|
@@ -93,12 +105,30 @@ import {
|
|
|
93
105
|
RECOMMEND_TARGET_URL
|
|
94
106
|
} from "./constants.js";
|
|
95
107
|
import {
|
|
96
|
-
clickRecommendActionControl,
|
|
97
|
-
normalizeRecommendPostAction,
|
|
98
|
-
resolveRecommendPostAction,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
clickRecommendActionControl,
|
|
109
|
+
normalizeRecommendPostAction,
|
|
110
|
+
resolveRecommendPostAction,
|
|
111
|
+
verifyRecommendActionControlIdentity,
|
|
112
|
+
waitForRecommendDetailActionControls
|
|
113
|
+
} from "./actions.js";
|
|
114
|
+
import { getRecommendRoots } from "./roots.js";
|
|
115
|
+
|
|
116
|
+
const RECOMMEND_GREETING_ACTION_FINGERPRINT = "boss-recommend-greet-action-v1";
|
|
117
|
+
|
|
118
|
+
function defaultRecommendActionJournalDir() {
|
|
119
|
+
const configuredHome = String(process.env.BOSS_RECOMMEND_HOME || "").trim();
|
|
120
|
+
const root = configuredHome
|
|
121
|
+
? path.resolve(configuredHome)
|
|
122
|
+
: path.join(os.homedir(), ".boss-recommend-mcp");
|
|
123
|
+
return path.join(root, "recommend-action-journal");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createRecommendGreetingActionJournal(options = {}) {
|
|
127
|
+
return createChatActionJournal({
|
|
128
|
+
baseDir: options.baseDir || defaultRecommendActionJournalDir(),
|
|
129
|
+
now: options.now
|
|
130
|
+
});
|
|
131
|
+
}
|
|
102
132
|
|
|
103
133
|
const RECOMMEND_DEBUG_BOUNDARY_MODES = Object.freeze({
|
|
104
134
|
list_end: "debug_force_list_end_after_processed",
|
|
@@ -463,59 +493,24 @@ export async function acquireRecommendListReadWithStaleRecovery({
|
|
|
463
493
|
}
|
|
464
494
|
}
|
|
465
495
|
|
|
466
|
-
export async function recoverRecommendListReadStaleContext({
|
|
467
|
-
staleAttempt = 1,
|
|
468
|
-
listState,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const noCardsError = new Error("Recommend list root reacquire returned no candidate cards");
|
|
485
|
-
noCardsError.code = "RECOMMEND_LIST_ROOT_REACQUIRE_NO_CARDS";
|
|
486
|
-
throw noCardsError;
|
|
487
|
-
}
|
|
488
|
-
resetInfiniteListForRefreshRound(listState, {
|
|
489
|
-
reason: "list_read_stale_node_root_reacquire",
|
|
490
|
-
round: 1,
|
|
491
|
-
method: "root_reacquire",
|
|
492
|
-
metadata: {
|
|
493
|
-
card_count: Number(rootResult?.card_count) || 0,
|
|
494
|
-
processed_count: processedKeys.size
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
return {
|
|
498
|
-
recovery_mode: "root_reacquire",
|
|
499
|
-
root_reacquire: rootResult || null
|
|
500
|
-
};
|
|
501
|
-
} catch (rootReacquireError) {
|
|
502
|
-
const contextResult = await contextReapply({ rootReacquireError });
|
|
503
|
-
return {
|
|
504
|
-
recovery_mode: "context_reapply",
|
|
505
|
-
escalated_from: "root_reacquire",
|
|
506
|
-
root_reacquire_error: compactListReadStaleDiagnostic(rootReacquireError, {
|
|
507
|
-
attempt: 1
|
|
508
|
-
}),
|
|
509
|
-
context_reapply: contextResult || null
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const contextResult = await contextReapply({ rootReacquireError: null });
|
|
514
|
-
return {
|
|
515
|
-
recovery_mode: "context_reapply",
|
|
516
|
-
escalated_from: "repeated_stale",
|
|
517
|
-
context_reapply: contextResult || null
|
|
518
|
-
};
|
|
496
|
+
export async function recoverRecommendListReadStaleContext({
|
|
497
|
+
staleAttempt = 1,
|
|
498
|
+
listState,
|
|
499
|
+
contextReapply
|
|
500
|
+
} = {}) {
|
|
501
|
+
if (typeof contextReapply !== "function") {
|
|
502
|
+
throw new Error("recoverRecommendListReadStaleContext requires contextReapply");
|
|
503
|
+
}
|
|
504
|
+
const processedKeys = new Set(listState?.processed_keys || []);
|
|
505
|
+
try {
|
|
506
|
+
const contextResult = await contextReapply({ rootReacquireError: null });
|
|
507
|
+
return {
|
|
508
|
+
recovery_mode: "context_reapply",
|
|
509
|
+
escalated_from: Number(staleAttempt) <= 1
|
|
510
|
+
? "root_only_recovery_disallowed"
|
|
511
|
+
: "repeated_stale",
|
|
512
|
+
context_reapply: contextResult || null
|
|
513
|
+
};
|
|
519
514
|
} finally {
|
|
520
515
|
for (const key of processedKeys) listState?.processed_keys?.add(key);
|
|
521
516
|
}
|
|
@@ -547,13 +542,13 @@ function normalizeFilter(filter = {}) {
|
|
|
547
542
|
labels: normalizeLabels(filter.labels || filter.filterLabels || []),
|
|
548
543
|
selectAllLabels: Boolean(filter.selectAllLabels),
|
|
549
544
|
allowUnlimited: filter.allowUnlimited === true,
|
|
550
|
-
verifySticky:
|
|
545
|
+
verifySticky: true,
|
|
551
546
|
filterGroups: filterGroups.map((group) => ({
|
|
552
547
|
group: String(group?.group || ""),
|
|
553
548
|
labels: normalizeLabels(group?.labels || group?.filterLabels || []),
|
|
554
549
|
selectAllLabels: group?.selectAllLabels !== false,
|
|
555
550
|
allowUnlimited: group?.allowUnlimited === true,
|
|
556
|
-
verifySticky:
|
|
551
|
+
verifySticky: true
|
|
557
552
|
})).filter((group) => group.group || group.labels.length)
|
|
558
553
|
};
|
|
559
554
|
}
|
|
@@ -718,24 +713,67 @@ function compactScreening(screening) {
|
|
|
718
713
|
};
|
|
719
714
|
}
|
|
720
715
|
|
|
721
|
-
function compactCandidate(candidate) {
|
|
716
|
+
function compactCandidate(candidate) {
|
|
722
717
|
return {
|
|
723
718
|
id: candidate?.id || null,
|
|
724
719
|
identity: candidate?.identity || {},
|
|
725
720
|
text_length: candidate?.text?.raw?.length || 0,
|
|
726
721
|
tag_count: candidate?.tags?.length || 0
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function normalizeRecommendScreeningIdentityValue(value = "") {
|
|
726
|
+
return String(value || "").replace(/\s+/g, " ").trim().normalize("NFKC");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function assertRecommendScreeningCandidateMatchesCard({
|
|
730
|
+
cardCandidate = null,
|
|
731
|
+
screeningCandidate = null,
|
|
732
|
+
stage = "before_screening"
|
|
733
|
+
} = {}) {
|
|
734
|
+
const expectedCandidateId = normalizeRecommendScreeningIdentityValue(cardCandidate?.id);
|
|
735
|
+
const actualCandidateId = normalizeRecommendScreeningIdentityValue(screeningCandidate?.id);
|
|
736
|
+
const expectedName = normalizeRecommendScreeningIdentityValue(cardCandidate?.identity?.name);
|
|
737
|
+
const actualName = normalizeRecommendScreeningIdentityValue(screeningCandidate?.identity?.name);
|
|
738
|
+
const diagnostic = {
|
|
739
|
+
schema_version: 1,
|
|
740
|
+
verified: Boolean(
|
|
741
|
+
expectedCandidateId
|
|
742
|
+
&& actualCandidateId === expectedCandidateId
|
|
743
|
+
&& expectedName
|
|
744
|
+
&& actualName === expectedName
|
|
745
|
+
),
|
|
746
|
+
stage,
|
|
747
|
+
expected_candidate_id: expectedCandidateId || null,
|
|
748
|
+
actual_candidate_id: actualCandidateId || null,
|
|
749
|
+
exact_candidate_id: Boolean(expectedCandidateId && actualCandidateId === expectedCandidateId),
|
|
750
|
+
expected_name: expectedName || null,
|
|
751
|
+
actual_name: actualName || null,
|
|
752
|
+
exact_name: Boolean(expectedName && actualName === expectedName)
|
|
753
|
+
};
|
|
754
|
+
if (diagnostic.verified) return diagnostic;
|
|
755
|
+
|
|
756
|
+
const error = new Error(
|
|
757
|
+
`RECOMMEND_SCREENING_CANDIDATE_IDENTITY_MISMATCH: stage=${stage}; expected=${expectedCandidateId || "missing"}/${expectedName || "missing"}; actual=${actualCandidateId || "missing"}/${actualName || "missing"}`
|
|
758
|
+
);
|
|
759
|
+
error.code = "RECOMMEND_SCREENING_CANDIDATE_IDENTITY_MISMATCH";
|
|
760
|
+
error.phase = "recommend:screening-candidate-identity";
|
|
761
|
+
error.screening_candidate_identity = diagnostic;
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function compactDetail(detailResult) {
|
|
731
766
|
if (!detailResult) return null;
|
|
732
767
|
return {
|
|
733
768
|
popup_text_length: detailResult.detail?.popup_text?.length || 0,
|
|
734
769
|
resume_text_length: detailResult.detail?.resume_text?.length || 0,
|
|
735
|
-
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
736
|
-
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
737
|
-
|
|
738
|
-
|
|
770
|
+
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
771
|
+
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
772
|
+
network_profile_binding: detailResult.network_profile_binding || null,
|
|
773
|
+
screening_candidate_identity: detailResult.screening_candidate_identity || null,
|
|
774
|
+
cv_acquisition: detailResult.cv_acquisition || null,
|
|
775
|
+
candidate_binding: compactRecommendDetailCandidateBinding(detailResult.candidate_binding),
|
|
776
|
+
colleague_contact: detailResult.colleague_contact || null,
|
|
739
777
|
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
740
778
|
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
741
779
|
close_result: detailResult.close_result
|
|
@@ -753,26 +791,348 @@ function createMissingLlmConfigResult() {
|
|
|
753
791
|
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
|
|
754
792
|
}
|
|
755
793
|
|
|
756
|
-
function compactActionDiscovery(discovery) {
|
|
794
|
+
function compactActionDiscovery(discovery) {
|
|
757
795
|
if (!discovery) return null;
|
|
758
796
|
return {
|
|
759
797
|
elapsed_ms: discovery.elapsed_ms,
|
|
760
798
|
timed_out: Boolean(discovery.timed_out),
|
|
761
799
|
detail_root_count: discovery.detail_root_count || 0,
|
|
762
800
|
summary: discovery.summary || null
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
async function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export async function selectAndVerifyInitialRecommendJob(client, rootState, {
|
|
805
|
+
jobLabel = "",
|
|
806
|
+
settleMs = 6000,
|
|
807
|
+
dropdownTimeoutMs = Math.max(8000, settleMs),
|
|
808
|
+
totalTimeoutMs = Math.max(30000, dropdownTimeoutMs + settleMs + 4000),
|
|
809
|
+
retryDelayMs = 1000,
|
|
810
|
+
selectWithRootRefresh = selectRecommendJobWithRootRefresh
|
|
811
|
+
} = {}) {
|
|
812
|
+
if (!jobLabel) {
|
|
813
|
+
throw new Error("selectAndVerifyInitialRecommendJob requires a requested job label");
|
|
814
|
+
}
|
|
815
|
+
if (typeof selectWithRootRefresh !== "function") {
|
|
816
|
+
throw new Error("selectAndVerifyInitialRecommendJob requires selectWithRootRefresh");
|
|
817
|
+
}
|
|
818
|
+
const result = await selectWithRootRefresh(client, rootState, {
|
|
819
|
+
jobLabel,
|
|
820
|
+
settleMs,
|
|
821
|
+
dropdownTimeoutMs,
|
|
822
|
+
totalTimeoutMs,
|
|
823
|
+
retryDelayMs
|
|
824
|
+
});
|
|
825
|
+
const selection = result?.job_selection || null;
|
|
826
|
+
const sticky = selection?.sticky_verification || null;
|
|
827
|
+
if (
|
|
828
|
+
selection?.selected !== true
|
|
829
|
+
|| sticky?.verified !== true
|
|
830
|
+
|| sticky?.menu_close?.ok === false
|
|
831
|
+
) {
|
|
832
|
+
const error = new Error(
|
|
833
|
+
`Requested recommend job was not independently sticky-verified: requested=${jobLabel}; current=${sticky?.current_label_without_salary || sticky?.current_label || "unknown"}`
|
|
834
|
+
);
|
|
835
|
+
error.code = "RECOMMEND_INITIAL_JOB_STICKY_VERIFICATION_FAILED";
|
|
836
|
+
error.job_selection = compactJobSelection(selection);
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function compactRecommendActionJournalRecord(record = null, filePath = null) {
|
|
843
|
+
if (!record) return null;
|
|
844
|
+
const history = Array.isArray(record.history) ? record.history : [];
|
|
845
|
+
const last = history[history.length - 1] || null;
|
|
846
|
+
return {
|
|
847
|
+
domain: "recommend",
|
|
848
|
+
schema_version: record.schema_version || 1,
|
|
849
|
+
action_key: record.action_key || null,
|
|
850
|
+
candidate_id: record.candidate_id || null,
|
|
851
|
+
state: record.state || null,
|
|
852
|
+
revision: Number.isSafeInteger(Number(record.revision)) ? Number(record.revision) : history.length,
|
|
853
|
+
unknown_origin: record.state === "outcome_unknown" ? last?.from_state || null : null,
|
|
854
|
+
evidence: record.evidence || {},
|
|
855
|
+
created_at: record.created_at || null,
|
|
856
|
+
updated_at: record.updated_at || null,
|
|
857
|
+
first_run_id: record.first_run_id || null,
|
|
858
|
+
last_run_id: record.last_run_id || null,
|
|
859
|
+
file_path: filePath || null
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function recommendGreetingUnknownOrigin(record = null) {
|
|
864
|
+
const history = Array.isArray(record?.history) ? record.history : [];
|
|
865
|
+
const last = history[history.length - 1];
|
|
866
|
+
return record?.state === "outcome_unknown" ? last?.from_state || null : null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function latestRecommendGreetingEvidence(record = null) {
|
|
870
|
+
const history = Array.isArray(record?.history) ? record.history : [];
|
|
871
|
+
return history[history.length - 1]?.evidence || {};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function isVerifiedRecommendActionJournalScope(scope = "") {
|
|
875
|
+
return /^boss-recommend-profile-v2:127\.0\.0\.1:profile-sha256:[0-9a-f]{64}$/u.test(
|
|
876
|
+
String(scope || "").trim()
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function compactRecommendControlEvidence(control = null) {
|
|
881
|
+
const rect = control?.rect || {};
|
|
882
|
+
const center = control?.center || {};
|
|
883
|
+
return {
|
|
884
|
+
control_node_id: Number.isInteger(Number(control?.node_id)) ? Number(control.node_id) : null,
|
|
885
|
+
control_backend_node_id: Number.isInteger(Number(control?.backend_node_id))
|
|
886
|
+
? Number(control.backend_node_id)
|
|
887
|
+
: null,
|
|
888
|
+
control_root: control?.root || null,
|
|
889
|
+
control_label: String(control?.label || "").trim() || null,
|
|
890
|
+
control_root_node_id: Number.isInteger(Number(control?.root_node_id))
|
|
891
|
+
? Number(control.root_node_id)
|
|
892
|
+
: null,
|
|
893
|
+
control_root_backend_node_id: Number.isInteger(Number(control?.root_backend_node_id))
|
|
894
|
+
? Number(control.root_backend_node_id)
|
|
895
|
+
: null,
|
|
896
|
+
control_center_x: Number.isFinite(Number(center.x)) ? Number(center.x) : null,
|
|
897
|
+
control_center_y: Number.isFinite(Number(center.y)) ? Number(center.y) : null,
|
|
898
|
+
control_rect_x: Number.isFinite(Number(rect.x)) ? Number(rect.x) : null,
|
|
899
|
+
control_rect_y: Number.isFinite(Number(rect.y)) ? Number(rect.y) : null,
|
|
900
|
+
control_rect_width: Number.isFinite(Number(rect.width)) ? Number(rect.width) : null,
|
|
901
|
+
control_rect_height: Number.isFinite(Number(rect.height)) ? Number(rect.height) : null
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function compactRecommendPostActionError(error) {
|
|
906
|
+
if (!error) return null;
|
|
907
|
+
return {
|
|
908
|
+
code: error.code || "RECOMMEND_GREETING_CLICK_FAILED",
|
|
909
|
+
message: String(error.message || error).slice(0, 500),
|
|
910
|
+
phase: error.phase || null,
|
|
911
|
+
cdp_method: error.cdp_method || null,
|
|
912
|
+
cdp_at: error.cdp_at || null,
|
|
913
|
+
cdp_node_id: Number.isInteger(error.cdp_node_id) ? error.cdp_node_id : null,
|
|
914
|
+
cdp_outcome_unknown: error.cdp_outcome_unknown === true,
|
|
915
|
+
recommend_pre_input_aborted: error.recommend_pre_input_aborted === true,
|
|
916
|
+
recommend_input_dispatched: error.recommend_input_dispatched === true,
|
|
917
|
+
recommend_action_control_hit_test: error.recommend_action_control_hit_test || null
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function checkpointRecommendPostActionStopResult(runControl, checkpoint = {}, {
|
|
922
|
+
candidateResult = null,
|
|
923
|
+
candidateId = "",
|
|
924
|
+
actionState = null,
|
|
925
|
+
resultIndex = null
|
|
926
|
+
} = {}) {
|
|
927
|
+
try {
|
|
928
|
+
return runControl.checkpointCritical(checkpoint);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
runControl.checkpoint({
|
|
931
|
+
...checkpoint,
|
|
932
|
+
preserve_detail_on_terminal: true,
|
|
933
|
+
action_result_critical_persisted: false,
|
|
934
|
+
terminal_preservation: {
|
|
935
|
+
required: true,
|
|
936
|
+
reason: "post_action_candidate_result_critical_persistence_failed",
|
|
937
|
+
candidate_id: String(candidateId || "").trim() || null,
|
|
938
|
+
action_state: actionState || null,
|
|
939
|
+
result_index: Number.isInteger(resultIndex) ? resultIndex : null,
|
|
940
|
+
error: compactRecommendPostActionError(error)
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
error.code = error.code || "RECOMMEND_POST_ACTION_RESULT_PERSISTENCE_FAILED";
|
|
944
|
+
error.phase = error.phase || "recommend:post-action-result-persistence";
|
|
945
|
+
error.recommend_preserve_detail_on_terminal = true;
|
|
946
|
+
error.recommend_candidate_result = candidateResult;
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function isVerifiedRecommendPostActionCandidateBinding(binding = null, candidateId = "") {
|
|
952
|
+
const expectedCandidateId = String(candidateId || "").trim();
|
|
953
|
+
const root = binding?.detail?.root;
|
|
954
|
+
const rootNodeId = Number(root?.node_id);
|
|
955
|
+
const rootBackendNodeId = Number(root?.backend_node_id);
|
|
956
|
+
const exactRoot = Boolean(
|
|
957
|
+
root?.stable === true
|
|
958
|
+
&& root?.visible === true
|
|
959
|
+
&& root?.canonical === true
|
|
960
|
+
&& root?.action_root === true
|
|
961
|
+
&& Number.isInteger(rootNodeId)
|
|
962
|
+
&& rootNodeId > 0
|
|
963
|
+
&& Number.isInteger(rootBackendNodeId)
|
|
964
|
+
&& rootBackendNodeId > 0
|
|
965
|
+
);
|
|
966
|
+
const firstScopes = Array.isArray(binding?.detail?.first?.scopes)
|
|
967
|
+
? binding.detail.first.scopes
|
|
968
|
+
: [];
|
|
969
|
+
const secondScopes = Array.isArray(binding?.detail?.second?.scopes)
|
|
970
|
+
? binding.detail.second.scopes
|
|
971
|
+
: [];
|
|
972
|
+
const sampleMatchesRoot = (scopes) => {
|
|
973
|
+
const visiblePopups = scopes.filter((scope) => (
|
|
974
|
+
scope?.source === "popup" && scope?.visible === true
|
|
975
|
+
));
|
|
976
|
+
const visibleIframes = scopes.filter((scope) => (
|
|
977
|
+
scope?.source === "resume_iframe" && scope?.visible === true
|
|
978
|
+
));
|
|
979
|
+
if (visiblePopups.length > 1 || visibleIframes.length > 1) return false;
|
|
980
|
+
const canonicalScopes = root?.source === "popup" ? visiblePopups : visibleIframes;
|
|
981
|
+
if (
|
|
982
|
+
canonicalScopes.length !== 1
|
|
983
|
+
|| Number(canonicalScopes[0]?.node_id) !== rootNodeId
|
|
984
|
+
|| Number(canonicalScopes[0]?.backend_node_id) !== rootBackendNodeId
|
|
985
|
+
) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
if (root?.source === "popup") {
|
|
989
|
+
const contained = root?.contained_iframe || null;
|
|
990
|
+
if (!contained) return visibleIframes.length === 0;
|
|
991
|
+
if (visibleIframes.length !== 1) return false;
|
|
992
|
+
const iframe = visibleIframes[0];
|
|
993
|
+
return Boolean(
|
|
994
|
+
contained?.stable === true
|
|
995
|
+
&& contained?.contained === true
|
|
996
|
+
&& iframe?.container_verified === true
|
|
997
|
+
&& Number(iframe?.node_id) === Number(contained?.node_id)
|
|
998
|
+
&& Number(iframe?.backend_node_id) === Number(contained?.backend_node_id)
|
|
999
|
+
&& Number(iframe?.iframe_node_id) === Number(contained?.iframe_node_id)
|
|
1000
|
+
&& Number(iframe?.iframe_backend_node_id) === Number(contained?.iframe_backend_node_id)
|
|
1001
|
+
&& Number(iframe?.container_node_id) === rootNodeId
|
|
1002
|
+
&& Number(iframe?.container_backend_node_id) === rootBackendNodeId
|
|
1003
|
+
&& iframe?.selector === contained?.selector
|
|
1004
|
+
&& JSON.stringify(iframe?.ancestry?.path || [])
|
|
1005
|
+
=== JSON.stringify(contained?.ancestry_path || [])
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return visiblePopups.length === 0 && visibleIframes.length === 1;
|
|
1009
|
+
};
|
|
1010
|
+
const exactStableRootSamples = Boolean(
|
|
1011
|
+
exactRoot
|
|
1012
|
+
&& sampleMatchesRoot(firstScopes)
|
|
1013
|
+
&& sampleMatchesRoot(secondScopes)
|
|
1014
|
+
);
|
|
1015
|
+
const method = binding?.method;
|
|
1016
|
+
const compactBeforeCard = binding?.card?.before;
|
|
1017
|
+
const provenanceBeforeCard = binding?.card?.pre_click_provenance?.card;
|
|
1018
|
+
const causalBeforeCard = compactBeforeCard && provenanceBeforeCard
|
|
1019
|
+
? {
|
|
1020
|
+
...compactBeforeCard,
|
|
1021
|
+
candidate_id: compactBeforeCard.candidate_id ?? provenanceBeforeCard.candidate_id,
|
|
1022
|
+
name: compactBeforeCard.name ?? provenanceBeforeCard.name
|
|
1023
|
+
}
|
|
1024
|
+
: compactBeforeCard || provenanceBeforeCard;
|
|
1025
|
+
const causalProof = method === "exact_card_click_and_new_resume_root"
|
|
1026
|
+
? verifyExactCardClickToNewResumeRootCausality({
|
|
1027
|
+
cardNodeId: causalBeforeCard?.node_id,
|
|
1028
|
+
expectedCandidateId: binding?.expected_candidate_id,
|
|
1029
|
+
expectedName: binding?.expected_name,
|
|
1030
|
+
beforeCard: causalBeforeCard,
|
|
1031
|
+
afterCard: binding?.card?.after,
|
|
1032
|
+
cardPreClickProvenance: binding?.card?.pre_click_provenance,
|
|
1033
|
+
cardClickEvidence: binding?.card?.click_evidence,
|
|
1034
|
+
clickAttempts: binding?.card?.click_attempts,
|
|
1035
|
+
detailRoot: binding?.detail?.root,
|
|
1036
|
+
rootsBeforeWereCaptured: binding?.detail?.roots_before_capture?.captured === true,
|
|
1037
|
+
rootsBeforeCaptureComplete: binding?.detail?.roots_before_capture?.complete === true,
|
|
1038
|
+
newlyMounted: binding?.detail?.newly_mounted === true,
|
|
1039
|
+
rootMatchesExpected: binding?.detail?.root_matches_expected === true,
|
|
1040
|
+
hasCandidateIdEvidence: binding?.detail?.candidate_id_evidence_present === true,
|
|
1041
|
+
candidateIdProbeComplete: binding?.detail?.candidate_id_probe_complete === true
|
|
1042
|
+
})
|
|
1043
|
+
: null;
|
|
1044
|
+
const exactDetailIdentity = method === "exact_candidate_id_and_name"
|
|
1045
|
+
? binding?.detail?.candidate_id_probe_complete === true
|
|
1046
|
+
&& binding?.detail?.exact_candidate_id === true
|
|
1047
|
+
&& binding?.detail?.exact_name === true
|
|
1048
|
+
: method === "exact_name_and_secondary_identity"
|
|
1049
|
+
? binding?.detail?.candidate_id_probe_complete === true
|
|
1050
|
+
&& binding?.detail?.candidate_id_evidence_present === false
|
|
1051
|
+
&& binding?.detail?.exact_name === true
|
|
1052
|
+
&& binding?.detail?.exact_secondary === true
|
|
1053
|
+
: method === "exact_card_click_and_new_resume_root"
|
|
1054
|
+
? binding?.detail?.candidate_id_probe_complete === true
|
|
1055
|
+
&& binding?.detail?.candidate_id_evidence_present === false
|
|
1056
|
+
&& binding?.detail?.exact_name === false
|
|
1057
|
+
&& binding?.card?.causal_proof?.verified === true
|
|
1058
|
+
&& causalProof?.verified === true
|
|
1059
|
+
: false;
|
|
1060
|
+
return Boolean(
|
|
1061
|
+
expectedCandidateId
|
|
1062
|
+
&& binding?.verified === true
|
|
1063
|
+
&& binding?.stable === true
|
|
1064
|
+
&& binding?.expected_candidate_id === expectedCandidateId
|
|
1065
|
+
&& binding?.card?.stable === true
|
|
1066
|
+
&& binding?.card?.candidate_id === expectedCandidateId
|
|
1067
|
+
&& exactStableRootSamples
|
|
1068
|
+
&& exactDetailIdentity
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function assertRecommendControlMatchesCandidateDetailRoot(
|
|
1073
|
+
control,
|
|
1074
|
+
binding,
|
|
1075
|
+
stage = "post_action"
|
|
1076
|
+
) {
|
|
1077
|
+
const boundRoot = binding?.detail?.root;
|
|
1078
|
+
const boundRootNodeId = Number(boundRoot?.node_id);
|
|
1079
|
+
const boundRootBackendNodeId = Number(boundRoot?.backend_node_id);
|
|
1080
|
+
const controlRootNodeId = Number(control?.root_node_id);
|
|
1081
|
+
const controlRootBackendNodeId = Number(control?.root_backend_node_id);
|
|
1082
|
+
const exact = Boolean(
|
|
1083
|
+
Number.isInteger(boundRootNodeId)
|
|
1084
|
+
&& boundRootNodeId > 0
|
|
1085
|
+
&& Number.isInteger(boundRootBackendNodeId)
|
|
1086
|
+
&& boundRootBackendNodeId > 0
|
|
1087
|
+
&& boundRoot?.stable === true
|
|
1088
|
+
&& boundRoot?.visible === true
|
|
1089
|
+
&& boundRoot?.canonical === true
|
|
1090
|
+
&& boundRoot?.action_root === true
|
|
1091
|
+
&& Number.isInteger(controlRootNodeId)
|
|
1092
|
+
&& controlRootNodeId > 0
|
|
1093
|
+
&& Number.isInteger(controlRootBackendNodeId)
|
|
1094
|
+
&& controlRootBackendNodeId > 0
|
|
1095
|
+
// DOM.getDocument may remap a frontend node id while describeNode still
|
|
1096
|
+
// proves the same immutable backend node. The action path re-describes
|
|
1097
|
+
// this control root and proves control ancestry again before every Input.
|
|
1098
|
+
&& controlRootBackendNodeId === boundRootBackendNodeId
|
|
1099
|
+
);
|
|
1100
|
+
if (!exact) {
|
|
1101
|
+
const error = new Error(
|
|
1102
|
+
`RECOMMEND_ACTION_DETAIL_ROOT_MISMATCH: action control is not bound to the exact candidate detail root at ${stage}`
|
|
1103
|
+
);
|
|
1104
|
+
error.code = "RECOMMEND_ACTION_DETAIL_ROOT_MISMATCH";
|
|
1105
|
+
error.phase = "recommend:post-action-binding";
|
|
1106
|
+
error.expected_root_node_id = boundRootNodeId || null;
|
|
1107
|
+
error.expected_root_backend_node_id = boundRootBackendNodeId || null;
|
|
1108
|
+
error.observed_root_node_id = Number.isInteger(controlRootNodeId) ? controlRootNodeId : null;
|
|
1109
|
+
error.observed_root_backend_node_id = Number.isInteger(controlRootBackendNodeId)
|
|
1110
|
+
? controlRootBackendNodeId
|
|
1111
|
+
: null;
|
|
1112
|
+
error.retryable = false;
|
|
1113
|
+
throw error;
|
|
1114
|
+
}
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
export async function runRecommendPostAction({
|
|
1119
|
+
client,
|
|
1120
|
+
screening,
|
|
1121
|
+
actionDiscovery,
|
|
1122
|
+
postAction = "none",
|
|
1123
|
+
greetCount = 0,
|
|
1124
|
+
maxGreetCount = null,
|
|
1125
|
+
executePostAction = true,
|
|
1126
|
+
afterClickDelayMs = 900,
|
|
1127
|
+
candidateId = "",
|
|
1128
|
+
actionJournal = null,
|
|
1129
|
+
actionJournalScope = "boss-recommend:default",
|
|
1130
|
+
reverifyActionJournalScope = null,
|
|
1131
|
+
runId = "",
|
|
1132
|
+
candidateBinding = null,
|
|
1133
|
+
reverifyCandidateBinding = null,
|
|
1134
|
+
checkpointCritical = null
|
|
1135
|
+
} = {}) {
|
|
776
1136
|
const plan = resolveRecommendPostAction({
|
|
777
1137
|
postAction,
|
|
778
1138
|
greetCount,
|
|
@@ -806,10 +1166,10 @@ async function runRecommendPostAction({
|
|
|
806
1166
|
}
|
|
807
1167
|
result.control = control;
|
|
808
1168
|
|
|
809
|
-
if (plan.effective === "greet" && control.continue_chat) {
|
|
810
|
-
result.reason = "already_connected_continue_chat";
|
|
811
|
-
result.already_connected = true;
|
|
812
|
-
return result;
|
|
1169
|
+
if (plan.effective === "greet" && control.continue_chat && !executePostAction) {
|
|
1170
|
+
result.reason = "already_connected_continue_chat";
|
|
1171
|
+
result.already_connected = true;
|
|
1172
|
+
return result;
|
|
813
1173
|
}
|
|
814
1174
|
if (plan.effective === "greet" && control.greet_quota?.exhausted) {
|
|
815
1175
|
result.reason = "greet_credits_exhausted";
|
|
@@ -817,7 +1177,7 @@ async function runRecommendPostAction({
|
|
|
817
1177
|
result.stop_run = true;
|
|
818
1178
|
return result;
|
|
819
1179
|
}
|
|
820
|
-
if (plan.effective === "greet" && control.available === false) {
|
|
1180
|
+
if (plan.effective === "greet" && control.available === false && !control.continue_chat) {
|
|
821
1181
|
result.reason = "greet_control_not_available";
|
|
822
1182
|
return result;
|
|
823
1183
|
}
|
|
@@ -825,70 +1185,583 @@ async function runRecommendPostAction({
|
|
|
825
1185
|
result.reason = `${plan.effective}_control_disabled`;
|
|
826
1186
|
return result;
|
|
827
1187
|
}
|
|
828
|
-
if (!executePostAction) {
|
|
829
|
-
result.reason = "dry_run_post_action";
|
|
830
|
-
result.would_click = true;
|
|
831
|
-
return result;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
1188
|
+
if (!executePostAction) {
|
|
1189
|
+
result.reason = "dry_run_post_action";
|
|
1190
|
+
result.would_click = true;
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const normalizedCandidateId = String(candidateId || "").trim();
|
|
1195
|
+
if (!normalizedCandidateId) {
|
|
1196
|
+
const error = new Error(
|
|
1197
|
+
"RECOMMEND_ACTION_CANDIDATE_ID_REQUIRED: refusing greeting without a stable Boss candidate ID"
|
|
1198
|
+
);
|
|
1199
|
+
error.code = "RECOMMEND_ACTION_CANDIDATE_ID_REQUIRED";
|
|
1200
|
+
error.retryable = false;
|
|
1201
|
+
throw error;
|
|
1202
|
+
}
|
|
1203
|
+
if (!isVerifiedRecommendPostActionCandidateBinding(candidateBinding, normalizedCandidateId)) {
|
|
1204
|
+
const error = createRecommendDetailCandidateBindingError(candidateBinding || {
|
|
1205
|
+
schema_version: 1,
|
|
1206
|
+
verified: false,
|
|
1207
|
+
reason: "post_action_candidate_binding_missing",
|
|
1208
|
+
expected_candidate_id: normalizedCandidateId
|
|
1209
|
+
});
|
|
1210
|
+
error.phase = "recommend:post-action-binding";
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
result.candidate_binding = compactRecommendDetailCandidateBinding(candidateBinding);
|
|
1214
|
+
assertRecommendControlMatchesCandidateDetailRoot(
|
|
1215
|
+
control,
|
|
1216
|
+
candidateBinding,
|
|
1217
|
+
"initial_control_binding"
|
|
1218
|
+
);
|
|
1219
|
+
if (!isVerifiedRecommendActionJournalScope(actionJournalScope)) {
|
|
1220
|
+
const error = new Error(
|
|
1221
|
+
"RECOMMEND_ACTION_SCOPE_UNVERIFIED: live greeting requires an exact canonical Chrome profile scope"
|
|
1222
|
+
);
|
|
1223
|
+
error.code = "RECOMMEND_ACTION_SCOPE_UNVERIFIED";
|
|
1224
|
+
error.retryable = false;
|
|
1225
|
+
throw error;
|
|
1226
|
+
}
|
|
1227
|
+
if (typeof reverifyCandidateBinding !== "function") {
|
|
1228
|
+
const error = new Error(
|
|
1229
|
+
"RECOMMEND_ACTION_CANDIDATE_REVERIFY_REQUIRED: live greeting requires fresh candidate binding"
|
|
1230
|
+
);
|
|
1231
|
+
error.code = "RECOMMEND_ACTION_CANDIDATE_REVERIFY_REQUIRED";
|
|
1232
|
+
error.retryable = false;
|
|
1233
|
+
throw error;
|
|
1234
|
+
}
|
|
1235
|
+
if (typeof reverifyActionJournalScope !== "function") {
|
|
1236
|
+
const error = new Error(
|
|
1237
|
+
"RECOMMEND_ACTION_SCOPE_REVERIFY_REQUIRED: live greeting requires fresh Chrome profile proof"
|
|
1238
|
+
);
|
|
1239
|
+
error.code = "RECOMMEND_ACTION_SCOPE_REVERIFY_REQUIRED";
|
|
1240
|
+
error.retryable = false;
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
if (!actionJournal || typeof actionJournal.read !== "function" || typeof actionJournal.transition !== "function") {
|
|
1244
|
+
const error = new Error(
|
|
1245
|
+
"RECOMMEND_ACTION_JOURNAL_REQUIRED: durable greeting action journal is unavailable"
|
|
1246
|
+
);
|
|
1247
|
+
error.code = "RECOMMEND_ACTION_JOURNAL_REQUIRED";
|
|
1248
|
+
error.retryable = false;
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
if (typeof checkpointCritical !== "function") {
|
|
1252
|
+
const error = new Error(
|
|
1253
|
+
"RECOMMEND_CRITICAL_CHECKPOINT_UNAVAILABLE: refusing greeting without required persistence"
|
|
1254
|
+
);
|
|
1255
|
+
error.code = "RECOMMEND_CRITICAL_CHECKPOINT_UNAVAILABLE";
|
|
1256
|
+
error.retryable = false;
|
|
1257
|
+
throw error;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const requireFreshActionJournalScope = async (stage) => {
|
|
1261
|
+
const fresh = await reverifyActionJournalScope(stage);
|
|
1262
|
+
const freshScope = String(fresh?.scope || fresh || "").trim();
|
|
1263
|
+
if (!isVerifiedRecommendActionJournalScope(freshScope) || freshScope !== actionJournalScope) {
|
|
1264
|
+
const error = new Error(
|
|
1265
|
+
`RECOMMEND_ACTION_SCOPE_DRIFT: exact Chrome profile scope changed at ${stage}`
|
|
1266
|
+
);
|
|
1267
|
+
error.code = "RECOMMEND_ACTION_SCOPE_DRIFT";
|
|
1268
|
+
error.expected_scope = actionJournalScope;
|
|
1269
|
+
error.observed_scope = freshScope || null;
|
|
1270
|
+
error.retryable = false;
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
return freshScope;
|
|
1274
|
+
};
|
|
1275
|
+
const operationId = `${String(runId || "recommend-run").trim() || "recommend-run"}:${crypto.randomUUID()}`;
|
|
1276
|
+
const requireFreshCandidateBinding = async (stage, boundControl = control, {
|
|
1277
|
+
allowScroll = true,
|
|
1278
|
+
settleMs = 120
|
|
1279
|
+
} = {}) => {
|
|
1280
|
+
const normalizedSettleMs = allowScroll
|
|
1281
|
+
? Math.max(0, Number(settleMs) || 0)
|
|
1282
|
+
: 0;
|
|
1283
|
+
const freshBinding = await reverifyCandidateBinding(stage, {
|
|
1284
|
+
allowScroll: allowScroll === true,
|
|
1285
|
+
settleMs: normalizedSettleMs
|
|
1286
|
+
});
|
|
1287
|
+
if (!isVerifiedRecommendPostActionCandidateBinding(freshBinding, normalizedCandidateId)) {
|
|
1288
|
+
const error = createRecommendDetailCandidateBindingError(freshBinding || {
|
|
1289
|
+
schema_version: 1,
|
|
1290
|
+
verified: false,
|
|
1291
|
+
reason: `post_action_candidate_binding_lost_${stage}`,
|
|
1292
|
+
expected_candidate_id: normalizedCandidateId
|
|
1293
|
+
});
|
|
1294
|
+
error.phase = "recommend:post-action-binding";
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
if (
|
|
1298
|
+
allowScroll === false
|
|
1299
|
+
&& (
|
|
1300
|
+
freshBinding?.allow_scroll !== false
|
|
1301
|
+
|| Number(freshBinding?.settle_ms) !== 0
|
|
1302
|
+
)
|
|
1303
|
+
) {
|
|
1304
|
+
const error = createRecommendDetailCandidateBindingError({
|
|
1305
|
+
...freshBinding,
|
|
1306
|
+
verified: false,
|
|
1307
|
+
reason: "non_scrolling_candidate_reproof_unavailable"
|
|
1308
|
+
});
|
|
1309
|
+
error.phase = "recommend:post-action-binding";
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
assertRecommendControlMatchesCandidateDetailRoot(boundControl, freshBinding, stage);
|
|
1313
|
+
result.candidate_binding = compactRecommendDetailCandidateBinding(freshBinding);
|
|
1314
|
+
return freshBinding;
|
|
1315
|
+
};
|
|
1316
|
+
await requireFreshActionJournalScope("before_journal_read");
|
|
1317
|
+
let journalRecord = actionJournal.read({
|
|
1318
|
+
scope: actionJournalScope,
|
|
1319
|
+
candidateId: normalizedCandidateId
|
|
1320
|
+
});
|
|
1321
|
+
let journalFilePath = typeof actionJournal.entryPath === "function"
|
|
1322
|
+
? actionJournal.entryPath({ scope: actionJournalScope, candidateId: normalizedCandidateId })
|
|
1323
|
+
: null;
|
|
1324
|
+
const transitionAction = (state, evidence = {}, {
|
|
1325
|
+
requireChanged = false,
|
|
1326
|
+
recordIdempotent = false,
|
|
1327
|
+
expectedUpdatedAt = null,
|
|
1328
|
+
expectedRevision = null
|
|
1329
|
+
} = {}) => {
|
|
1330
|
+
const transitioned = actionJournal.transition({
|
|
1331
|
+
scope: actionJournalScope,
|
|
1332
|
+
candidateId: normalizedCandidateId,
|
|
1333
|
+
state,
|
|
1334
|
+
runId,
|
|
1335
|
+
greeting: RECOMMEND_GREETING_ACTION_FINGERPRINT,
|
|
1336
|
+
recordIdempotent,
|
|
1337
|
+
expectedUpdatedAt,
|
|
1338
|
+
expectedRevision,
|
|
1339
|
+
evidence: {
|
|
1340
|
+
action: "recommend_greet",
|
|
1341
|
+
active_candidate_id: normalizedCandidateId,
|
|
1342
|
+
operation_id: operationId,
|
|
1343
|
+
...evidence
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
if (requireChanged && transitioned?.changed !== true) {
|
|
1347
|
+
const error = new Error(
|
|
1348
|
+
"RECOMMEND_ACTION_IN_FLIGHT_NOT_OWNED: journal transition was idempotent or owned elsewhere"
|
|
1349
|
+
);
|
|
1350
|
+
error.code = "RECOMMEND_ACTION_IN_FLIGHT_NOT_OWNED";
|
|
1351
|
+
error.retryable = false;
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
journalRecord = transitioned.record;
|
|
1355
|
+
journalFilePath = transitioned.file_path || journalFilePath;
|
|
1356
|
+
const transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1357
|
+
try {
|
|
1358
|
+
checkpointCritical({ action_transaction: transaction });
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (state === "greeting_send_in_flight" && transitioned?.changed === true) {
|
|
1361
|
+
error.recommend_pre_input_checkpoint_failed = true;
|
|
1362
|
+
error.recommend_owned_operation_id = operationId;
|
|
1363
|
+
throw error;
|
|
1364
|
+
}
|
|
1365
|
+
if (state === "greeting_confirmed" || state === "outcome_unknown") {
|
|
1366
|
+
result.action_transaction_checkpoint_error = compactRecommendPostActionError(error);
|
|
1367
|
+
result.preserve_detail_until_result_persisted = true;
|
|
1368
|
+
result.stop_run = true;
|
|
1369
|
+
} else {
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
result.action_transaction = transaction;
|
|
1374
|
+
return journalRecord;
|
|
1375
|
+
};
|
|
1376
|
+
const preservePostInputJournalFailure = ({
|
|
1377
|
+
error,
|
|
1378
|
+
desiredState,
|
|
1379
|
+
reason,
|
|
1380
|
+
triggeringError = null,
|
|
1381
|
+
inputDispatched = true
|
|
1382
|
+
}) => {
|
|
1383
|
+
const journalError = compactRecommendPostActionError(error);
|
|
1384
|
+
const triggeringActionError = compactRecommendPostActionError(triggeringError);
|
|
1385
|
+
const lastDurableTransaction = compactRecommendActionJournalRecord(
|
|
1386
|
+
journalRecord,
|
|
1387
|
+
journalFilePath
|
|
1388
|
+
);
|
|
1389
|
+
const terminalPreservation = {
|
|
1390
|
+
required: true,
|
|
1391
|
+
reason: "post_input_action_journal_persistence_failed",
|
|
1392
|
+
candidate_id: normalizedCandidateId,
|
|
1393
|
+
operation_id: operationId,
|
|
1394
|
+
action: "recommend_greet",
|
|
1395
|
+
input_dispatched: inputDispatched === true,
|
|
1396
|
+
desired_action_state: desiredState || null,
|
|
1397
|
+
last_durable_action_state: lastDurableTransaction?.state || null,
|
|
1398
|
+
candidate_binding: result.candidate_binding
|
|
1399
|
+
|| compactRecommendDetailCandidateBinding(candidateBinding),
|
|
1400
|
+
control: compactRecommendControlEvidence(result.control_confirmation || control),
|
|
1401
|
+
click_result: result.click_result || null,
|
|
1402
|
+
journal_error: journalError,
|
|
1403
|
+
triggering_error: triggeringActionError
|
|
1404
|
+
};
|
|
1405
|
+
result.reason = reason || "greet_post_input_journal_persistence_failed";
|
|
1406
|
+
result.counted_as_greet = false;
|
|
1407
|
+
result.outcome_unknown = true;
|
|
1408
|
+
result.stop_run = true;
|
|
1409
|
+
result.preserve_detail_on_terminal = true;
|
|
1410
|
+
result.preserve_detail_until_result_persisted = true;
|
|
1411
|
+
result.post_input_journal_persistence_failed = true;
|
|
1412
|
+
result.action_input_dispatched = inputDispatched === true;
|
|
1413
|
+
result.action_transaction = lastDurableTransaction;
|
|
1414
|
+
result.terminal_preservation = terminalPreservation;
|
|
1415
|
+
result.error = journalError;
|
|
1416
|
+
if (triggeringActionError) result.triggering_error = triggeringActionError;
|
|
1417
|
+
try {
|
|
1418
|
+
checkpointCritical({
|
|
1419
|
+
preserve_detail_on_terminal: true,
|
|
1420
|
+
action_result_critical_persisted: false,
|
|
1421
|
+
terminal_preservation: terminalPreservation,
|
|
1422
|
+
action_transaction: lastDurableTransaction
|
|
1423
|
+
});
|
|
1424
|
+
result.terminal_preservation_checkpointed = true;
|
|
1425
|
+
} catch (checkpointError) {
|
|
1426
|
+
result.terminal_preservation_checkpointed = false;
|
|
1427
|
+
result.terminal_preservation_checkpoint_error = compactRecommendPostActionError(
|
|
1428
|
+
checkpointError
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return result;
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
if (control.continue_chat) {
|
|
1435
|
+
await requireFreshCandidateBinding("before_continue_chat_reconciliation", control);
|
|
1436
|
+
const reconciledControl = await verifyRecommendActionControlIdentity(client, control, {
|
|
1437
|
+
requireContinueChat: true,
|
|
1438
|
+
requireGeometry: true
|
|
1439
|
+
});
|
|
1440
|
+
await requireFreshCandidateBinding("after_continue_chat_control_verification", reconciledControl);
|
|
1441
|
+
result.control_reconciliation = reconciledControl;
|
|
1442
|
+
const unknownOrigin = recommendGreetingUnknownOrigin(journalRecord);
|
|
1443
|
+
if (
|
|
1444
|
+
journalRecord?.state === "greeting_send_in_flight"
|
|
1445
|
+
|| (journalRecord?.state === "outcome_unknown" && unknownOrigin === "greeting_send_in_flight")
|
|
1446
|
+
) {
|
|
1447
|
+
transitionAction("greeting_confirmed", {
|
|
1448
|
+
reason: "reconciled_from_exact_continue_chat_control"
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
result.reason = journalRecord?.state === "greeting_confirmed"
|
|
1452
|
+
? "greeting_confirmed_by_durable_journal"
|
|
1453
|
+
: "already_connected_continue_chat";
|
|
1454
|
+
result.already_connected = true;
|
|
1455
|
+
result.verified_after_click = true;
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (journalRecord?.state === "greeting_confirmed") {
|
|
1460
|
+
result.reason = "greeting_confirmed_by_durable_journal";
|
|
1461
|
+
result.already_connected = true;
|
|
1462
|
+
result.verified_after_click = true;
|
|
1463
|
+
result.action_transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1464
|
+
return result;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const unknownOrigin = recommendGreetingUnknownOrigin(journalRecord);
|
|
1468
|
+
const latestGreetingReason = latestRecommendGreetingEvidence(journalRecord)?.reason;
|
|
1469
|
+
const replayablePreInputAbort = Boolean(
|
|
1470
|
+
journalRecord?.state === "greeting_send_in_flight"
|
|
1471
|
+
&& (
|
|
1472
|
+
latestGreetingReason === "pre_input_checkpoint_aborted"
|
|
1473
|
+
|| latestGreetingReason === "pre_input_abort"
|
|
1474
|
+
)
|
|
1475
|
+
);
|
|
1476
|
+
if (
|
|
1477
|
+
(journalRecord?.state === "greeting_send_in_flight" && !replayablePreInputAbort)
|
|
1478
|
+
|| (journalRecord?.state === "outcome_unknown" && unknownOrigin === "greeting_send_in_flight")
|
|
1479
|
+
) {
|
|
1480
|
+
if (journalRecord.state !== "outcome_unknown") {
|
|
1481
|
+
transitionAction("outcome_unknown", {
|
|
1482
|
+
reason: "in_flight_greeting_not_passively_reconciled"
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
result.action_transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1486
|
+
checkpointCritical({ action_transaction: result.action_transaction });
|
|
1487
|
+
}
|
|
1488
|
+
result.reason = "greet_outcome_unknown_preserved_no_replay";
|
|
1489
|
+
result.outcome_unknown = true;
|
|
1490
|
+
result.skipped = true;
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1493
|
+
if (journalRecord?.state === "outcome_unknown") {
|
|
1494
|
+
result.reason = "greet_outcome_unknown_preserved_no_replay";
|
|
1495
|
+
result.outcome_unknown = true;
|
|
1496
|
+
result.skipped = true;
|
|
1497
|
+
result.action_transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1498
|
+
checkpointCritical({ action_transaction: result.action_transaction });
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
if (!journalRecord) {
|
|
1502
|
+
transitionAction("pre_action", { reason: "greeting_eligible" });
|
|
1503
|
+
}
|
|
1504
|
+
if (journalRecord?.state !== "pre_action" && !replayablePreInputAbort) {
|
|
1505
|
+
const error = new Error(
|
|
1506
|
+
`RECOMMEND_ACTION_JOURNAL_STATE_UNSUPPORTED: ${journalRecord?.state || "unknown"}`
|
|
1507
|
+
);
|
|
1508
|
+
error.code = "RECOMMEND_ACTION_JOURNAL_STATE_UNSUPPORTED";
|
|
1509
|
+
error.retryable = false;
|
|
1510
|
+
throw error;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
result.action_attempted = true;
|
|
1514
|
+
result.control_before = control;
|
|
1515
|
+
let clickResult;
|
|
1516
|
+
let greetingInFlightPersisted = false;
|
|
1517
|
+
try {
|
|
1518
|
+
clickResult = await clickRecommendActionControl(client, {
|
|
1519
|
+
...control,
|
|
1520
|
+
kind: plan.effective
|
|
1521
|
+
}, {
|
|
1522
|
+
beforeFinalRefresh: async () => {
|
|
1523
|
+
await requireFreshActionJournalScope("immediately_before_greeting_in_flight");
|
|
1524
|
+
await requireFreshCandidateBinding("immediately_before_greeting_control_refresh", control);
|
|
1525
|
+
},
|
|
1526
|
+
beforeClick: async (freshControl) => {
|
|
1527
|
+
const expectedUpdatedAt = journalRecord?.updated_at || null;
|
|
1528
|
+
const expectedRevision = Number(journalRecord?.revision);
|
|
1529
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
|
|
1530
|
+
const error = new Error(
|
|
1531
|
+
"RECOMMEND_ACTION_JOURNAL_REVISION_REQUIRED: exact journal ownership revision is missing"
|
|
1532
|
+
);
|
|
1533
|
+
error.code = "RECOMMEND_ACTION_JOURNAL_REVISION_REQUIRED";
|
|
1534
|
+
error.retryable = false;
|
|
1535
|
+
throw error;
|
|
1536
|
+
}
|
|
1537
|
+
transitionAction("greeting_send_in_flight", {
|
|
1538
|
+
send_method: "Input.dispatchMouseEvent",
|
|
1539
|
+
reason: "immediately_before_non_replayable_click",
|
|
1540
|
+
active_candidate_id: normalizedCandidateId,
|
|
1541
|
+
...compactRecommendControlEvidence(freshControl)
|
|
1542
|
+
}, {
|
|
1543
|
+
requireChanged: true,
|
|
1544
|
+
recordIdempotent: replayablePreInputAbort,
|
|
1545
|
+
expectedUpdatedAt,
|
|
1546
|
+
expectedRevision
|
|
1547
|
+
});
|
|
1548
|
+
greetingInFlightPersisted = true;
|
|
1549
|
+
},
|
|
1550
|
+
beforeInput: async () => {
|
|
1551
|
+
await requireFreshActionJournalScope("before_final_greeting_control_verification");
|
|
1552
|
+
await requireFreshCandidateBinding("before_final_greeting_control_verification", control);
|
|
1553
|
+
},
|
|
1554
|
+
immediatelyBeforeInput: async (finalControl) => {
|
|
1555
|
+
await requireFreshActionJournalScope("immediately_before_greeting_input");
|
|
1556
|
+
await requireFreshCandidateBinding(
|
|
1557
|
+
"immediately_before_greeting_input",
|
|
1558
|
+
finalControl,
|
|
1559
|
+
{ allowScroll: false, settleMs: 0 }
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
} catch (error) {
|
|
843
1564
|
if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
|
|
844
1565
|
result.reason = "greet_credits_exhausted";
|
|
845
1566
|
result.out_of_greet_credits = true;
|
|
846
1567
|
result.stop_run = true;
|
|
847
|
-
result.greet_quota = error.greet_quota || control.greet_quota || null;
|
|
848
|
-
return result;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
1568
|
+
result.greet_quota = error.greet_quota || control.greet_quota || null;
|
|
1569
|
+
return result;
|
|
1570
|
+
}
|
|
1571
|
+
if (
|
|
1572
|
+
error?.recommend_pre_input_checkpoint_failed === true
|
|
1573
|
+
|| (
|
|
1574
|
+
error?.recommend_pre_input_aborted === true
|
|
1575
|
+
&& journalRecord?.state === "greeting_send_in_flight"
|
|
1576
|
+
&& error?.code !== "RECOMMEND_ACTION_IN_FLIGHT_NOT_OWNED"
|
|
1577
|
+
&& error?.code !== "CHAT_ACTION_JOURNAL_CONCURRENT_UPDATE"
|
|
1578
|
+
)
|
|
1579
|
+
) {
|
|
1580
|
+
const abortReason = error?.recommend_pre_input_checkpoint_failed === true
|
|
1581
|
+
? "pre_input_checkpoint_aborted"
|
|
1582
|
+
: "pre_input_abort";
|
|
1583
|
+
const hitTestEvidence = error?.recommend_action_control_hit_test || null;
|
|
1584
|
+
const hitTestAttempts = Array.isArray(hitTestEvidence?.attempts)
|
|
1585
|
+
? hitTestEvidence.attempts
|
|
1586
|
+
: [];
|
|
1587
|
+
const lastHitTestAttempt = hitTestAttempts.at(-1) || null;
|
|
1588
|
+
const aborted = actionJournal.transition({
|
|
1589
|
+
scope: actionJournalScope,
|
|
1590
|
+
candidateId: normalizedCandidateId,
|
|
1591
|
+
state: "greeting_send_in_flight",
|
|
1592
|
+
runId,
|
|
1593
|
+
greeting: RECOMMEND_GREETING_ACTION_FINGERPRINT,
|
|
1594
|
+
recordIdempotent: true,
|
|
1595
|
+
expectedUpdatedAt: journalRecord?.updated_at || null,
|
|
1596
|
+
expectedRevision: journalRecord?.revision ?? null,
|
|
1597
|
+
evidence: {
|
|
1598
|
+
action: "recommend_greet",
|
|
1599
|
+
active_candidate_id: normalizedCandidateId,
|
|
1600
|
+
operation_id: operationId,
|
|
1601
|
+
reason: abortReason,
|
|
1602
|
+
send_method: "Input.dispatchMouseEvent",
|
|
1603
|
+
pre_input_cdp_method: error?.cdp_method || null,
|
|
1604
|
+
action_hit_test_reason: lastHitTestAttempt?.reason || null,
|
|
1605
|
+
action_hit_test_attempt_count: hitTestAttempts.length,
|
|
1606
|
+
action_hit_test_last_hit_backend_node_id: Number.isInteger(
|
|
1607
|
+
Number(lastHitTestAttempt?.hit_backend_node_id)
|
|
1608
|
+
) ? Number(lastHitTestAttempt.hit_backend_node_id) : null
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
journalRecord = aborted.record;
|
|
1612
|
+
journalFilePath = aborted.file_path || journalFilePath;
|
|
1613
|
+
result.action_transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1614
|
+
result.reason = abortReason === "pre_input_checkpoint_aborted"
|
|
1615
|
+
? "greet_pre_input_checkpoint_aborted_replayable"
|
|
1616
|
+
: "greet_pre_input_aborted_replayable";
|
|
1617
|
+
result.pre_input_aborted = true;
|
|
1618
|
+
result.replayable = true;
|
|
1619
|
+
result.stop_run = true;
|
|
1620
|
+
result.error = compactRecommendPostActionError(error);
|
|
1621
|
+
return result;
|
|
1622
|
+
}
|
|
1623
|
+
if (
|
|
1624
|
+
error?.code === "RECOMMEND_ACTION_IN_FLIGHT_NOT_OWNED"
|
|
1625
|
+
|| error?.code === "CHAT_ACTION_JOURNAL_CONCURRENT_UPDATE"
|
|
1626
|
+
) {
|
|
1627
|
+
journalRecord = actionJournal.read({
|
|
1628
|
+
scope: actionJournalScope,
|
|
1629
|
+
candidateId: normalizedCandidateId
|
|
1630
|
+
}) || journalRecord;
|
|
1631
|
+
result.reason = "greet_in_flight_owned_by_another_operation";
|
|
1632
|
+
result.outcome_unknown = true;
|
|
1633
|
+
result.skipped = true;
|
|
1634
|
+
result.stop_run = true;
|
|
1635
|
+
result.action_transaction = compactRecommendActionJournalRecord(journalRecord, journalFilePath);
|
|
1636
|
+
return result;
|
|
1637
|
+
}
|
|
1638
|
+
if (greetingInFlightPersisted || journalRecord?.state === "greeting_send_in_flight") {
|
|
1639
|
+
try {
|
|
1640
|
+
transitionAction("outcome_unknown", {
|
|
1641
|
+
reason: "non_replayable_click_failed_or_disconnected",
|
|
1642
|
+
send_method: error?.cdp_method || "Input.dispatchMouseEvent"
|
|
1643
|
+
});
|
|
1644
|
+
} catch (journalError) {
|
|
1645
|
+
return preservePostInputJournalFailure({
|
|
1646
|
+
error: journalError,
|
|
1647
|
+
desiredState: "outcome_unknown",
|
|
1648
|
+
reason: "greet_post_input_outcome_unknown_persistence_failed",
|
|
1649
|
+
triggeringError: error,
|
|
1650
|
+
inputDispatched: true
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
result.reason = "greet_outcome_unknown";
|
|
1654
|
+
result.outcome_unknown = true;
|
|
1655
|
+
result.stop_run = true;
|
|
1656
|
+
result.error = compactRecommendPostActionError(error);
|
|
1657
|
+
return result;
|
|
1658
|
+
}
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
852
1661
|
result.click_result = clickResult;
|
|
853
1662
|
result.action_clicked = true;
|
|
854
1663
|
result.counted_as_greet = plan.effective === "greet";
|
|
855
1664
|
result.reason = "clicked";
|
|
856
1665
|
if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
|
|
857
1666
|
try {
|
|
858
|
-
const afterDiscovery = await waitForRecommendDetailActionControls(client, {
|
|
859
|
-
timeoutMs: 2500,
|
|
860
|
-
intervalMs: 300,
|
|
861
|
-
requireAny: false
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const
|
|
1667
|
+
const afterDiscovery = await waitForRecommendDetailActionControls(client, {
|
|
1668
|
+
timeoutMs: 2500,
|
|
1669
|
+
intervalMs: 300,
|
|
1670
|
+
requireAny: false,
|
|
1671
|
+
requireContinueChat: plan.effective === "greet"
|
|
1672
|
+
});
|
|
1673
|
+
const afterSummary = afterDiscovery?.summary || {};
|
|
1674
|
+
const afterControl = afterSummary.greet;
|
|
865
1675
|
result.action_discovery_after = compactActionDiscovery(afterDiscovery);
|
|
866
|
-
result.control_after = afterControl || null;
|
|
867
|
-
if (plan.effective === "greet") {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1676
|
+
result.control_after = afterControl || null;
|
|
1677
|
+
if (plan.effective === "greet") {
|
|
1678
|
+
await requireFreshCandidateBinding("after_greeting_before_confirmation", afterControl);
|
|
1679
|
+
const confirmedControl = await verifyRecommendActionControlIdentity(client, afterControl, {
|
|
1680
|
+
requireContinueChat: true,
|
|
1681
|
+
requireGeometry: true
|
|
1682
|
+
});
|
|
1683
|
+
await requireFreshCandidateBinding("after_greeting_control_confirmation", confirmedControl);
|
|
1684
|
+
result.control_confirmation = confirmedControl;
|
|
1685
|
+
result.verified_after_click = confirmedControl.verified === true;
|
|
872
1686
|
}
|
|
873
|
-
} catch (error) {
|
|
1687
|
+
} catch (error) {
|
|
874
1688
|
result.verify_error = {
|
|
875
|
-
message: error?.message || String(error)
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
|
|
1689
|
+
message: error?.message || String(error)
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
if (result.verified_after_click === true) {
|
|
1694
|
+
transitionAction("greeting_confirmed", {
|
|
1695
|
+
reason: "exact_continue_chat_control_after_click",
|
|
1696
|
+
send_method: "Input.dispatchMouseEvent"
|
|
1697
|
+
});
|
|
1698
|
+
result.counted_as_greet = plan.effective === "greet";
|
|
1699
|
+
result.reason = "greeting_confirmed";
|
|
1700
|
+
} else {
|
|
1701
|
+
transitionAction("outcome_unknown", {
|
|
1702
|
+
reason: "post_click_confirmation_not_observed",
|
|
1703
|
+
send_method: "Input.dispatchMouseEvent"
|
|
1704
|
+
});
|
|
1705
|
+
result.counted_as_greet = false;
|
|
1706
|
+
result.reason = "greet_outcome_unknown";
|
|
1707
|
+
result.outcome_unknown = true;
|
|
1708
|
+
result.stop_run = true;
|
|
1709
|
+
}
|
|
1710
|
+
} catch (journalError) {
|
|
1711
|
+
return preservePostInputJournalFailure({
|
|
1712
|
+
error: journalError,
|
|
1713
|
+
desiredState: result.verified_after_click === true
|
|
1714
|
+
? "greeting_confirmed"
|
|
1715
|
+
: "outcome_unknown",
|
|
1716
|
+
reason: "greet_post_input_terminal_state_persistence_failed",
|
|
1717
|
+
inputDispatched: true
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return result;
|
|
1721
|
+
}
|
|
880
1722
|
|
|
881
|
-
function
|
|
882
|
-
if (!
|
|
883
|
-
return {
|
|
884
|
-
|
|
885
|
-
|
|
1723
|
+
function compactRecommendFilteredEmptyState(emptyState) {
|
|
1724
|
+
if (!emptyState) return null;
|
|
1725
|
+
return {
|
|
1726
|
+
verified: Boolean(emptyState.verified),
|
|
1727
|
+
reason: emptyState.reason || null,
|
|
1728
|
+
text: emptyState.text || null,
|
|
1729
|
+
node_id: Number.isInteger(emptyState.node_id) ? emptyState.node_id : null,
|
|
1730
|
+
box: emptyState.box || null,
|
|
1731
|
+
accessibility: emptyState.accessibility || null,
|
|
1732
|
+
selector_counts: emptyState.selector_counts || null,
|
|
1733
|
+
checked_node_count: Number.isInteger(emptyState.checked_node_count)
|
|
1734
|
+
? emptyState.checked_node_count
|
|
1735
|
+
: 0,
|
|
1736
|
+
candidate_node_count: Number.isInteger(emptyState.candidate_node_count)
|
|
1737
|
+
? emptyState.candidate_node_count
|
|
1738
|
+
: 0,
|
|
1739
|
+
query_errors: Array.isArray(emptyState.query_errors) ? emptyState.query_errors.slice(0, 20) : []
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
export function isVerifiedRecommendRefreshCompletion(refreshResult) {
|
|
1744
|
+
return Boolean(
|
|
1745
|
+
refreshResult?.ok === true
|
|
1746
|
+
&& refreshResult?.exhausted === true
|
|
1747
|
+
&& refreshResult?.empty_state?.verified === true
|
|
1748
|
+
&& Number(refreshResult?.card_count) === 0
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function compactRefreshAttempt(refreshAttempt) {
|
|
1753
|
+
if (!refreshAttempt) return null;
|
|
1754
|
+
return {
|
|
1755
|
+
ok: Boolean(refreshAttempt.ok),
|
|
1756
|
+
exhausted: Boolean(refreshAttempt.exhausted),
|
|
1757
|
+
method: refreshAttempt.method || "",
|
|
886
1758
|
reason: refreshAttempt.reason || null,
|
|
887
1759
|
error: refreshAttempt.error || null,
|
|
888
1760
|
error_diagnostic: refreshAttempt.error_diagnostic || null,
|
|
889
1761
|
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
890
1762
|
target_url: refreshAttempt.target_url || null,
|
|
891
|
-
card_count: refreshAttempt.card_count
|
|
1763
|
+
card_count: Number.isInteger(refreshAttempt.card_count) ? refreshAttempt.card_count : 0,
|
|
1764
|
+
empty_state: compactRecommendFilteredEmptyState(refreshAttempt.empty_state),
|
|
892
1765
|
elapsed_ms: refreshAttempt.elapsed_ms || 0,
|
|
893
1766
|
recovery_settle: refreshAttempt.recovery_settle
|
|
894
1767
|
? {
|
|
@@ -905,9 +1778,10 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
905
1778
|
error: attempt.error || null,
|
|
906
1779
|
error_diagnostic: attempt.error_diagnostic || null,
|
|
907
1780
|
label: attempt.label || null,
|
|
908
|
-
before_card_count: attempt.before_card_count || 0,
|
|
909
|
-
after_card_count: attempt.after_card_count || 0,
|
|
910
|
-
card_count: attempt.card_count
|
|
1781
|
+
before_card_count: attempt.before_card_count || 0,
|
|
1782
|
+
after_card_count: attempt.after_card_count || 0,
|
|
1783
|
+
card_count: Number.isInteger(attempt.card_count) ? attempt.card_count : 0,
|
|
1784
|
+
empty_state: compactRecommendFilteredEmptyState(attempt.empty_state),
|
|
911
1785
|
elapsed_ms: attempt.elapsed_ms || 0,
|
|
912
1786
|
current_city_only: compactCurrentCityOnlyResult(attempt.current_city_only),
|
|
913
1787
|
current_city_only_attempts: (attempt.current_city_only_attempts || []).map((cityAttempt) => ({
|
|
@@ -956,9 +1830,9 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
956
1830
|
};
|
|
957
1831
|
}
|
|
958
1832
|
|
|
959
|
-
export function countRecommendResultStatuses(results = [], {
|
|
960
|
-
greetCount = 0
|
|
961
|
-
} = {}) {
|
|
1833
|
+
export function countRecommendResultStatuses(results = [], {
|
|
1834
|
+
greetCount = 0
|
|
1835
|
+
} = {}) {
|
|
962
1836
|
return {
|
|
963
1837
|
processed: results.length,
|
|
964
1838
|
screened: results.length,
|
|
@@ -969,17 +1843,18 @@ export function countRecommendResultStatuses(results = [], {
|
|
|
969
1843
|
greet_count: greetCount,
|
|
970
1844
|
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
971
1845
|
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
972
|
-
detail_open_failed: results.filter((item) => (
|
|
973
|
-
item.error?.code === "DETAIL_STALE_NODE"
|
|
974
|
-
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1846
|
+
detail_open_failed: results.filter((item) => (
|
|
1847
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
1848
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
1849
|
+
|| item.error?.code === "RECOMMEND_DETAIL_CANDIDATE_MISMATCH"
|
|
1850
|
+
)).length,
|
|
1851
|
+
transient_recovered: results.filter((item) => (
|
|
1852
|
+
(
|
|
1853
|
+
item.timings?.image_capture_resume?.attempted === true
|
|
1854
|
+
&& item.timings?.image_capture_resume?.ok === true
|
|
1855
|
+
)
|
|
1856
|
+
|| item.detail?.cv_acquisition?.close_recovery?.ok === true
|
|
1857
|
+
)).length,
|
|
983
1858
|
colleague_contact_checked: results.filter((item) => item.detail?.colleague_contact?.checked).length,
|
|
984
1859
|
recent_colleague_contact_skipped: results.filter((item) => (
|
|
985
1860
|
item.screening?.status === "skip"
|
|
@@ -1041,15 +1916,37 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
|
1041
1916
|
if (error.list_end_reason) {
|
|
1042
1917
|
result.list_end_reason = error.list_end_reason;
|
|
1043
1918
|
}
|
|
1044
|
-
if (error.target_count != null) {
|
|
1045
|
-
result.target_count = error.target_count;
|
|
1046
|
-
}
|
|
1047
|
-
if (error.
|
|
1919
|
+
if (error.target_count != null) {
|
|
1920
|
+
result.target_count = error.target_count;
|
|
1921
|
+
}
|
|
1922
|
+
if (error.screening_candidate_identity) {
|
|
1923
|
+
result.screening_candidate_identity = error.screening_candidate_identity;
|
|
1924
|
+
}
|
|
1925
|
+
if (error.passed_count != null) {
|
|
1048
1926
|
result.passed_count = error.passed_count;
|
|
1049
1927
|
}
|
|
1050
|
-
if (Array.isArray(error.recommend_detail_open_attempts)) {
|
|
1051
|
-
result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
|
|
1052
|
-
}
|
|
1928
|
+
if (Array.isArray(error.recommend_detail_open_attempts)) {
|
|
1929
|
+
result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
|
|
1930
|
+
}
|
|
1931
|
+
if (error.recommend_pre_click_retry) {
|
|
1932
|
+
result.recommend_pre_click_retry = error.recommend_pre_click_retry;
|
|
1933
|
+
}
|
|
1934
|
+
if (error.recommend_pre_click_stale_no_action === true) {
|
|
1935
|
+
result.recommend_pre_click_stale_no_action = true;
|
|
1936
|
+
result.recommend_no_click_dispatched = error.recommend_no_click_dispatched === true;
|
|
1937
|
+
result.recommend_click_dispatched = error.recommend_click_dispatched === true;
|
|
1938
|
+
result.recommend_input_dispatched = error.recommend_input_dispatched === true;
|
|
1939
|
+
result.recommend_pre_click_stage = error.recommend_pre_click_stage || null;
|
|
1940
|
+
result.recommend_pre_click_retry_exhausted = error.recommend_pre_click_retry_exhausted === true;
|
|
1941
|
+
result.recommend_pre_click_reacquire_failed = error.recommend_pre_click_reacquire_failed === true;
|
|
1942
|
+
}
|
|
1943
|
+
if (error.recommend_input_dispatched === true) {
|
|
1944
|
+
result.recommend_click_dispatched = error.recommend_click_dispatched === true;
|
|
1945
|
+
result.recommend_input_dispatched = true;
|
|
1946
|
+
result.recommend_post_input_outcome_unknown = error.recommend_post_input_outcome_unknown === true;
|
|
1947
|
+
result.recommend_click_negative_outcome_observed = error.recommend_click_negative_outcome_observed === true;
|
|
1948
|
+
result.recommend_post_input_stage = error.recommend_post_input_stage || null;
|
|
1949
|
+
}
|
|
1053
1950
|
if (Array.isArray(error.click_attempts)) {
|
|
1054
1951
|
result.click_attempts = error.click_attempts;
|
|
1055
1952
|
}
|
|
@@ -1135,6 +2032,7 @@ export function createRecommendRefreshFailureError(refreshAttempt, {
|
|
|
1135
2032
|
export function isRecoverableImageCaptureError(error) {
|
|
1136
2033
|
if (isRecoverableImageCaptureWorkflowError(error)) return true;
|
|
1137
2034
|
if (isStaleRecommendNodeError(error)) return true;
|
|
2035
|
+
if (error?.code === "IMAGE_CAPTURE_TARGET_UNAVAILABLE") return true;
|
|
1138
2036
|
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
1139
2037
|
}
|
|
1140
2038
|
|
|
@@ -1199,22 +2097,163 @@ function createImageCaptureFailureScreening(candidate, error) {
|
|
|
1199
2097
|
};
|
|
1200
2098
|
}
|
|
1201
2099
|
|
|
1202
|
-
export function isRecoverableRecommendDetailError(error) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
2100
|
+
export function isRecoverableRecommendDetailError(error) {
|
|
2101
|
+
if (isCandidateLocalRecommendPostClickBindingTimeout(error)) return true;
|
|
2102
|
+
if (
|
|
2103
|
+
error?.recommend_input_dispatched === true
|
|
2104
|
+
&& error?.recommend_click_negative_outcome_observed !== true
|
|
2105
|
+
) return false;
|
|
2106
|
+
return isRecommendPreClickStaleNoActionError(error)
|
|
2107
|
+
|| isStaleRecommendNodeError(error)
|
|
2108
|
+
|| isRecommendDetailOpenMissError(error)
|
|
2109
|
+
|| isRecommendDetailCandidateBindingError(error);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
export function isCandidateLocalRecommendPostClickBindingTimeout(error) {
|
|
2113
|
+
const binding = error?.detail_candidate_binding || null;
|
|
2114
|
+
const clickAttempts = Array.isArray(error?.click_attempts) ? error.click_attempts : [];
|
|
2115
|
+
const dispatchedAttempts = clickAttempts.filter(
|
|
2116
|
+
(attempt) => attempt?.input_dispatched === true
|
|
2117
|
+
);
|
|
2118
|
+
return Boolean(
|
|
2119
|
+
isRecommendDetailCandidateBindingError(error)
|
|
2120
|
+
&& error?.recommend_clean_pre_action_detail_binding_timeout === true
|
|
2121
|
+
&& error?.recommend_click_dispatched === true
|
|
2122
|
+
&& error?.recommend_input_dispatched === true
|
|
2123
|
+
&& error?.recommend_post_input_outcome_unknown === false
|
|
2124
|
+
&& error?.recommend_post_input_stage === "post_card_click_binding"
|
|
2125
|
+
&& binding?.reason === "detail_binding_readiness_timeout"
|
|
2126
|
+
&& binding?.readiness?.exhausted === true
|
|
2127
|
+
&& binding?.readiness?.terminal === false
|
|
2128
|
+
&& binding?.readiness?.last_error == null
|
|
2129
|
+
&& dispatchedAttempts.length === 1
|
|
2130
|
+
&& dispatchedAttempts[0]?.outcome === "detail"
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
export function getRecommendDetailFailureDisposition(error) {
|
|
2135
|
+
if (isCandidateLocalRecommendPostClickBindingTimeout(error)) {
|
|
2136
|
+
return {
|
|
2137
|
+
recoverable: true,
|
|
2138
|
+
candidate_local: true,
|
|
2139
|
+
context_recovery: false,
|
|
2140
|
+
allow_post_action: false,
|
|
2141
|
+
reason: "detail_binding_readiness_timeout_failed_closed"
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
if (
|
|
2145
|
+
error?.recommend_input_dispatched === true
|
|
2146
|
+
&& error?.recommend_click_negative_outcome_observed !== true
|
|
2147
|
+
) {
|
|
2148
|
+
return {
|
|
2149
|
+
recoverable: false,
|
|
2150
|
+
candidate_local: false,
|
|
2151
|
+
context_recovery: false,
|
|
2152
|
+
allow_post_action: false,
|
|
2153
|
+
reason: "post_input_detail_outcome_unknown_terminal"
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
if (isRecommendDetailCandidateBindingError(error)) {
|
|
2157
|
+
return {
|
|
2158
|
+
recoverable: true,
|
|
2159
|
+
candidate_local: true,
|
|
2160
|
+
context_recovery: false,
|
|
2161
|
+
allow_post_action: false,
|
|
2162
|
+
reason: "candidate_binding_failed_closed"
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
if (
|
|
2166
|
+
isRecommendPreClickStaleNoActionError(error)
|
|
2167
|
+
&& error?.recommend_pre_click_retry_exhausted === true
|
|
2168
|
+
&& error?.recommend_pre_click_reacquire_failed !== true
|
|
2169
|
+
&& error?.recommend_pre_click_retry?.candidate_local_exhaustion === true
|
|
2170
|
+
) {
|
|
2171
|
+
return {
|
|
2172
|
+
recoverable: true,
|
|
2173
|
+
candidate_local: true,
|
|
2174
|
+
context_recovery: false,
|
|
2175
|
+
allow_post_action: false,
|
|
2176
|
+
reason: "pre_click_stale_no_action_failed_closed"
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
if (isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error)) {
|
|
2180
|
+
return {
|
|
2181
|
+
recoverable: true,
|
|
2182
|
+
candidate_local: false,
|
|
2183
|
+
context_recovery: true,
|
|
2184
|
+
allow_post_action: false,
|
|
2185
|
+
reason: "detail_context_recovery_required"
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
return {
|
|
2189
|
+
recoverable: false,
|
|
2190
|
+
candidate_local: false,
|
|
2191
|
+
context_recovery: false,
|
|
2192
|
+
allow_post_action: false,
|
|
2193
|
+
reason: "fatal_detail_error"
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function compactRecoverableDetailError(error) {
|
|
2198
|
+
const compact = compactError(
|
|
2199
|
+
error,
|
|
2200
|
+
isRecommendPreClickStaleNoActionError(error)
|
|
2201
|
+
&& error?.recommend_pre_click_retry_exhausted === true
|
|
2202
|
+
? "RECOMMEND_PRE_CLICK_STALE_NO_ACTION"
|
|
2203
|
+
: isStaleRecommendNodeError(error)
|
|
2204
|
+
? "DETAIL_STALE_NODE"
|
|
2205
|
+
: isRecommendDetailCandidateBindingError(error)
|
|
2206
|
+
? "RECOMMEND_DETAIL_CANDIDATE_MISMATCH"
|
|
2207
|
+
: "DETAIL_OPEN_FAILED"
|
|
2208
|
+
);
|
|
2209
|
+
if (error?.detail_candidate_binding) {
|
|
2210
|
+
compact.candidate_binding = compactRecommendDetailCandidateBinding(error.detail_candidate_binding);
|
|
2211
|
+
}
|
|
2212
|
+
return compact;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
export function preserveRecommendDetailCandidateBindingForRecovery(detailResult, binding = null) {
|
|
2216
|
+
const preservedBinding = binding || null;
|
|
2217
|
+
if (detailResult && typeof detailResult === "object") {
|
|
2218
|
+
detailResult.candidate_binding = preservedBinding;
|
|
2219
|
+
}
|
|
2220
|
+
return preservedBinding;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
export function reserveRecommendDetailRecovery(recoveryCounts, candidateKey, limit = 1) {
|
|
2224
|
+
const boundedLimit = Math.max(0, Math.floor(Number(limit) || 0));
|
|
2225
|
+
const currentCount = Number(recoveryCounts?.get?.(candidateKey) || 0);
|
|
2226
|
+
if (currentCount >= boundedLimit) {
|
|
2227
|
+
return {
|
|
2228
|
+
allowed: false,
|
|
2229
|
+
current_count: currentCount,
|
|
2230
|
+
next_count: currentCount,
|
|
2231
|
+
limit: boundedLimit
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
recoveryCounts.set(candidateKey, currentCount + 1);
|
|
2235
|
+
return {
|
|
2236
|
+
allowed: true,
|
|
2237
|
+
current_count: currentCount,
|
|
2238
|
+
next_count: currentCount + 1,
|
|
2239
|
+
limit: boundedLimit
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
1209
2242
|
|
|
1210
|
-
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
2243
|
+
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
1211
2244
|
return {
|
|
1212
2245
|
status: "fail",
|
|
1213
2246
|
passed: false,
|
|
1214
2247
|
score: 0,
|
|
1215
|
-
reasons:
|
|
1216
|
-
|
|
1217
|
-
|
|
2248
|
+
reasons: isRecommendPreClickStaleNoActionError(error)
|
|
2249
|
+
&& error?.recommend_pre_click_retry_exhausted === true
|
|
2250
|
+
&& error?.recommend_pre_click_reacquire_failed !== true
|
|
2251
|
+
? ["detail_open_failed", "pre_click_stale_no_action"]
|
|
2252
|
+
: isStaleRecommendNodeError(error)
|
|
2253
|
+
? ["detail_open_failed", "stale_node"]
|
|
2254
|
+
: isRecommendDetailCandidateBindingError(error)
|
|
2255
|
+
? ["detail_open_failed", "detail_candidate_mismatch"]
|
|
2256
|
+
: isRecommendDetailOpenMissError(error)
|
|
1218
2257
|
? ["detail_open_failed", "detail_open_miss"]
|
|
1219
2258
|
: ["detail_open_failed"],
|
|
1220
2259
|
error: compactRecoverableDetailError(error),
|
|
@@ -1222,7 +2261,7 @@ function createRecoverableDetailFailureScreening(candidate, error) {
|
|
|
1222
2261
|
};
|
|
1223
2262
|
}
|
|
1224
2263
|
|
|
1225
|
-
function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
|
|
2264
|
+
function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
|
|
1226
2265
|
const matched = colleagueContact?.matched_row || null;
|
|
1227
2266
|
return {
|
|
1228
2267
|
status: "skip",
|
|
@@ -1232,8 +2271,287 @@ function createRecentColleagueContactSkipScreening(candidate, colleagueContact)
|
|
|
1232
2271
|
reason: matched?.text || "Candidate has recent colleague contact history",
|
|
1233
2272
|
matched_colleague_contact: matched,
|
|
1234
2273
|
candidate
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
export function isVerifiedColleagueContactInspection(colleagueContact) {
|
|
2278
|
+
const rows = Array.isArray(colleagueContact?.rows) ? colleagueContact.rows : [];
|
|
2279
|
+
const rowCount = Number(colleagueContact?.row_count);
|
|
2280
|
+
const sectionNodeId = Number(colleagueContact?.section_node_id);
|
|
2281
|
+
const sectionBackendNodeId = Number(colleagueContact?.section_backend_node_id);
|
|
2282
|
+
const reason = colleagueContact?.reason;
|
|
2283
|
+
if (reason === "panel_missing") {
|
|
2284
|
+
const absenceProbe = colleagueContact?.absence_probe;
|
|
2285
|
+
const backendNodeIds = Array.isArray(absenceProbe?.scope_backend_node_ids)
|
|
2286
|
+
? absenceProbe.scope_backend_node_ids
|
|
2287
|
+
: [];
|
|
2288
|
+
return Boolean(
|
|
2289
|
+
colleagueContact?.checked === true
|
|
2290
|
+
&& colleagueContact?.panel_found === false
|
|
2291
|
+
&& colleagueContact?.recent === false
|
|
2292
|
+
&& colleagueContact?.indeterminate === false
|
|
2293
|
+
&& absenceProbe?.verified === true
|
|
2294
|
+
&& absenceProbe?.selector === ".colleague-collaboration"
|
|
2295
|
+
&& Number.isInteger(Number(absenceProbe?.scope_count))
|
|
2296
|
+
&& Number(absenceProbe.scope_count) > 0
|
|
2297
|
+
&& Number.isInteger(Number(absenceProbe?.stable_scope_count))
|
|
2298
|
+
&& Number(absenceProbe.stable_scope_count) > 0
|
|
2299
|
+
&& Number(absenceProbe.stable_scope_count) === backendNodeIds.length
|
|
2300
|
+
&& backendNodeIds.every((backendNodeId) => (
|
|
2301
|
+
Number.isInteger(Number(backendNodeId)) && Number(backendNodeId) > 0
|
|
2302
|
+
))
|
|
2303
|
+
&& Number.isInteger(Number(absenceProbe?.poll_count))
|
|
2304
|
+
&& Number(absenceProbe.poll_count) >= 2
|
|
2305
|
+
&& Number(absenceProbe?.elapsed_ms) >= Number(absenceProbe?.timeout_ms)
|
|
2306
|
+
&& absenceProbe?.full_window_elapsed === true
|
|
2307
|
+
&& Number(absenceProbe?.query_error_count) === 0
|
|
2308
|
+
&& absenceProbe?.scope_binding_lost === false
|
|
2309
|
+
&& rows.length === 0
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
const scrollProbe = colleagueContact?.scroll_probe;
|
|
2313
|
+
const scrollPositions = Array.isArray(scrollProbe?.positions) ? scrollProbe.positions : [];
|
|
2314
|
+
const requestedScrolls = Number(scrollProbe?.scrolls_requested);
|
|
2315
|
+
const completedScrolls = Number(scrollProbe?.scrolls_completed);
|
|
2316
|
+
const endProof = scrollProbe?.end_proof;
|
|
2317
|
+
const seenRowTexts = new Set();
|
|
2318
|
+
let priorRowSignature = null;
|
|
2319
|
+
let priorRowLayoutSignature = null;
|
|
2320
|
+
let recomputedStableSignatureCount = 0;
|
|
2321
|
+
let recomputedEffectiveScrollCount = 0;
|
|
2322
|
+
const scrollPositionsVerified = scrollPositions.every((position, index) => {
|
|
2323
|
+
const rowTexts = Array.isArray(position?.row_texts)
|
|
2324
|
+
? position.row_texts.map((text) => String(text || "")).sort()
|
|
2325
|
+
: [];
|
|
2326
|
+
const rowIdentityKeys = Array.isArray(position?.row_identity_keys)
|
|
2327
|
+
? position.row_identity_keys.map((key) => String(key || "")).sort()
|
|
2328
|
+
: [];
|
|
2329
|
+
const rowBackendNodeIds = Array.isArray(position?.row_backend_node_ids)
|
|
2330
|
+
? position.row_backend_node_ids.map(Number)
|
|
2331
|
+
: [];
|
|
2332
|
+
const orderedRowLayout = Array.isArray(position?.ordered_row_layout)
|
|
2333
|
+
? position.ordered_row_layout.map((row) => ({
|
|
2334
|
+
backend_node_id: Number(row?.backend_node_id),
|
|
2335
|
+
text: String(row?.text || ""),
|
|
2336
|
+
x: Number(row?.x),
|
|
2337
|
+
y: Number(row?.y),
|
|
2338
|
+
width: Number(row?.width),
|
|
2339
|
+
height: Number(row?.height)
|
|
2340
|
+
}))
|
|
2341
|
+
: [];
|
|
2342
|
+
const recomputedOrderedRowLayout = orderedRowLayout.slice().sort((left, right) => (
|
|
2343
|
+
left.y - right.y
|
|
2344
|
+
|| left.x - right.x
|
|
2345
|
+
|| left.backend_node_id - right.backend_node_id
|
|
2346
|
+
));
|
|
2347
|
+
const orderedRowLayoutKeys = recomputedOrderedRowLayout.map((row) => (
|
|
2348
|
+
`${row.text}:${row.x}:${row.y}:${row.width}:${row.height}`
|
|
2349
|
+
));
|
|
2350
|
+
const persistedOrderedRowLayoutKeys = Array.isArray(position?.ordered_row_layout_keys)
|
|
2351
|
+
? position.ordered_row_layout_keys.map((key) => String(key || ""))
|
|
2352
|
+
: [];
|
|
2353
|
+
const expectedNewRowTexts = rowTexts.filter((text) => !seenRowTexts.has(text));
|
|
2354
|
+
const rowSignature = JSON.stringify(rowIdentityKeys);
|
|
2355
|
+
const rowLayoutSignature = JSON.stringify(orderedRowLayoutKeys);
|
|
2356
|
+
const expectedScrollEffectObserved = Boolean(
|
|
2357
|
+
index > 0
|
|
2358
|
+
&& rowLayoutSignature !== priorRowLayoutSignature
|
|
2359
|
+
);
|
|
2360
|
+
if (expectedScrollEffectObserved) recomputedEffectiveScrollCount += 1;
|
|
2361
|
+
if (
|
|
2362
|
+
index > 0
|
|
2363
|
+
&& rowSignature === priorRowSignature
|
|
2364
|
+
&& expectedNewRowTexts.length === 0
|
|
2365
|
+
) {
|
|
2366
|
+
recomputedStableSignatureCount += 1;
|
|
2367
|
+
} else {
|
|
2368
|
+
recomputedStableSignatureCount = 0;
|
|
2369
|
+
}
|
|
2370
|
+
const verified = Boolean(
|
|
2371
|
+
Number(position?.position_index) === index
|
|
2372
|
+
&& Number(position?.sampled_after_scroll_count) === index
|
|
2373
|
+
&& Number(position?.row_count) > 0
|
|
2374
|
+
&& rowTexts.length === Number(position?.row_count)
|
|
2375
|
+
&& new Set(rowTexts).size === rowTexts.length
|
|
2376
|
+
&& rowIdentityKeys.length === Number(position?.row_count)
|
|
2377
|
+
&& new Set(rowIdentityKeys).size === rowIdentityKeys.length
|
|
2378
|
+
&& rowBackendNodeIds.length === Number(position?.row_count)
|
|
2379
|
+
&& rowBackendNodeIds.every((backendNodeId) => Number.isInteger(backendNodeId) && backendNodeId > 0)
|
|
2380
|
+
&& position?.row_signature === rowSignature
|
|
2381
|
+
&& orderedRowLayout.length === Number(position?.row_count)
|
|
2382
|
+
&& orderedRowLayout.every((row) => (
|
|
2383
|
+
Number.isInteger(row.backend_node_id)
|
|
2384
|
+
&& row.backend_node_id > 0
|
|
2385
|
+
&& rowBackendNodeIds.includes(row.backend_node_id)
|
|
2386
|
+
&& row.text.length > 0
|
|
2387
|
+
&& rowTexts.includes(row.text)
|
|
2388
|
+
&& [row.x, row.y, row.width, row.height].every(Number.isFinite)
|
|
2389
|
+
&& row.width > 0
|
|
2390
|
+
&& row.height > 0
|
|
2391
|
+
))
|
|
2392
|
+
&& JSON.stringify(orderedRowLayout) === JSON.stringify(recomputedOrderedRowLayout)
|
|
2393
|
+
&& orderedRowLayoutKeys.length === Number(position?.row_count)
|
|
2394
|
+
&& new Set(orderedRowLayoutKeys).size === orderedRowLayoutKeys.length
|
|
2395
|
+
&& JSON.stringify(persistedOrderedRowLayoutKeys) === JSON.stringify(orderedRowLayoutKeys)
|
|
2396
|
+
&& position?.row_layout_signature === rowLayoutSignature
|
|
2397
|
+
&& position?.scroll_effect_observed === expectedScrollEffectObserved
|
|
2398
|
+
&& Number(position?.cumulative_effective_scroll_count) === recomputedEffectiveScrollCount
|
|
2399
|
+
&& Number(position?.new_row_count) === expectedNewRowTexts.length
|
|
2400
|
+
&& JSON.stringify(
|
|
2401
|
+
Array.isArray(position?.new_row_texts)
|
|
2402
|
+
? position.new_row_texts.map((text) => String(text || "")).sort()
|
|
2403
|
+
: []
|
|
2404
|
+
) === JSON.stringify([...expectedNewRowTexts].sort())
|
|
2405
|
+
&& Number(position?.stable_signature_count) === recomputedStableSignatureCount
|
|
2406
|
+
&& Number(position?.unreadable_row_count) === 0
|
|
2407
|
+
&& position?.binding_before_verified === true
|
|
2408
|
+
&& position?.binding_after_verified === true
|
|
2409
|
+
);
|
|
2410
|
+
for (const text of rowTexts) seenRowTexts.add(text);
|
|
2411
|
+
priorRowSignature = rowSignature;
|
|
2412
|
+
priorRowLayoutSignature = rowLayoutSignature;
|
|
2413
|
+
return verified;
|
|
2414
|
+
});
|
|
2415
|
+
const lastScrollPosition = scrollPositions[scrollPositions.length - 1] || null;
|
|
2416
|
+
const exactEndProofVerified = Boolean(
|
|
2417
|
+
endProof?.verified === true
|
|
2418
|
+
&& endProof?.method === "effective_scroll_then_repeated_identical_rows"
|
|
2419
|
+
&& Number(endProof?.stable_samples_required) === 2
|
|
2420
|
+
&& Number(endProof?.stable_samples_observed) >= 2
|
|
2421
|
+
&& Number(endProof?.additional_wheel_attempts_without_change) >= 2
|
|
2422
|
+
&& endProof?.effective_scroll_observed === true
|
|
2423
|
+
&& Number(endProof?.effective_scroll_count) === recomputedEffectiveScrollCount
|
|
2424
|
+
&& recomputedEffectiveScrollCount > 0
|
|
2425
|
+
&& Number(endProof?.end_position_index) === scrollPositions.length - 1
|
|
2426
|
+
&& Number(endProof?.end_scroll_count) === completedScrolls
|
|
2427
|
+
&& endProof?.row_signature === lastScrollPosition?.row_signature
|
|
2428
|
+
&& Number(lastScrollPosition?.stable_signature_count) >= 2
|
|
2429
|
+
&& scrollProbe?.cap_reached_without_end === false
|
|
2430
|
+
);
|
|
2431
|
+
const noRecentScanVerified = reason !== "no_recent_colleague_contact" || Boolean(
|
|
2432
|
+
scrollProbe?.completed === true
|
|
2433
|
+
&& scrollProbe?.coverage_verified === true
|
|
2434
|
+
&& Number.isInteger(requestedScrolls)
|
|
2435
|
+
&& requestedScrolls >= 2
|
|
2436
|
+
&& requestedScrolls <= 48
|
|
2437
|
+
&& Number.isInteger(completedScrolls)
|
|
2438
|
+
&& completedScrolls >= 2
|
|
2439
|
+
&& completedScrolls <= requestedScrolls
|
|
2440
|
+
&& Number(scrollProbe?.position_count) === completedScrolls + 1
|
|
2441
|
+
&& scrollPositions.length === completedScrolls + 1
|
|
2442
|
+
&& Number(scrollProbe?.step_delta_y) > 0
|
|
2443
|
+
&& Number(scrollProbe?.overlap_ratio) > 0
|
|
2444
|
+
&& Number(scrollProbe?.overlap_ratio) < 1
|
|
2445
|
+
&& Number(scrollProbe?.effective_scroll_count) === recomputedEffectiveScrollCount
|
|
2446
|
+
&& recomputedEffectiveScrollCount > 0
|
|
2447
|
+
&& scrollPositionsVerified
|
|
2448
|
+
&& exactEndProofVerified
|
|
2449
|
+
);
|
|
2450
|
+
const semanticResultMatchesRows = reason === "recent_colleague_contact_found"
|
|
2451
|
+
? colleagueContact?.recent === true && rows.some((row) => row?.within_window === true)
|
|
2452
|
+
: reason === "no_recent_colleague_contact"
|
|
2453
|
+
? colleagueContact?.recent === false && rows.every((row) => row?.within_window === false)
|
|
2454
|
+
: false;
|
|
2455
|
+
return Boolean(
|
|
2456
|
+
colleagueContact?.checked === true
|
|
2457
|
+
&& colleagueContact?.panel_found === true
|
|
2458
|
+
&& colleagueContact?.indeterminate !== true
|
|
2459
|
+
&& typeof colleagueContact?.recent === "boolean"
|
|
2460
|
+
&& semanticResultMatchesRows
|
|
2461
|
+
&& noRecentScanVerified
|
|
2462
|
+
&& String(colleagueContact?.selected_tab_text || "").replace(/\s+/g, "").trim() === "同事沟通进度"
|
|
2463
|
+
&& colleagueContact?.selected_tab_count === 1
|
|
2464
|
+
&& colleagueContact?.pane_binding_verified === true
|
|
2465
|
+
&& colleagueContact?.binding?.verified === true
|
|
2466
|
+
&& colleagueContact?.binding?.selection_reverified_after_rows === true
|
|
2467
|
+
&& colleagueContact?.binding?.row_scope === "selected_section_descendants"
|
|
2468
|
+
&& Number.isInteger(sectionNodeId)
|
|
2469
|
+
&& sectionNodeId > 0
|
|
2470
|
+
&& Number.isInteger(sectionBackendNodeId)
|
|
2471
|
+
&& sectionBackendNodeId > 0
|
|
2472
|
+
&& Number.isInteger(rowCount)
|
|
2473
|
+
&& rowCount > 0
|
|
2474
|
+
&& rows.length === rowCount
|
|
2475
|
+
&& (
|
|
2476
|
+
reason !== "no_recent_colleague_contact"
|
|
2477
|
+
|| (
|
|
2478
|
+
seenRowTexts.size === rowCount
|
|
2479
|
+
&& rows.every((row) => seenRowTexts.has(String(row?.text || "")))
|
|
2480
|
+
)
|
|
2481
|
+
)
|
|
2482
|
+
&& rows.every((row) => (
|
|
2483
|
+
Boolean(String(row?.text || "").trim())
|
|
2484
|
+
&& Boolean(row?.parsed_date)
|
|
2485
|
+
&& typeof row?.within_window === "boolean"
|
|
2486
|
+
&& row?.visible === true
|
|
2487
|
+
&& Number(row?.section_node_id) === sectionNodeId
|
|
2488
|
+
&& Number(row?.section_backend_node_id) === sectionBackendNodeId
|
|
2489
|
+
&& Number.isInteger(Number(row?.backend_node_id))
|
|
2490
|
+
&& Number(row?.backend_node_id) > 0
|
|
2491
|
+
&& (
|
|
2492
|
+
reason !== "no_recent_colleague_contact"
|
|
2493
|
+
|| (
|
|
2494
|
+
Array.isArray(row?.observed_at_positions)
|
|
2495
|
+
&& row.observed_at_positions.length > 0
|
|
2496
|
+
&& row.observed_at_positions.every((position) => (
|
|
2497
|
+
Number.isInteger(Number(position))
|
|
2498
|
+
&& Number(position) >= 0
|
|
2499
|
+
&& Number(position) <= completedScrolls
|
|
2500
|
+
))
|
|
2501
|
+
)
|
|
2502
|
+
)
|
|
2503
|
+
))
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
export function getColleagueContactSkipReason(colleagueContact) {
|
|
2508
|
+
if (!isVerifiedColleagueContactInspection(colleagueContact)) {
|
|
2509
|
+
return "colleague_contact_unverified";
|
|
2510
|
+
}
|
|
2511
|
+
return colleagueContact?.recent === true ? "skipped_recent_colleague_contact" : "";
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
export async function bindRecommendColleagueContactInspectionResult(
|
|
2515
|
+
colleagueContact,
|
|
2516
|
+
{ reverifyCandidateBinding } = {}
|
|
2517
|
+
) {
|
|
2518
|
+
if (typeof reverifyCandidateBinding !== "function") {
|
|
2519
|
+
const error = new Error("Recommend colleague-contact result requires fresh candidate binding verification");
|
|
2520
|
+
error.code = "RECOMMEND_DETAIL_CANDIDATE_BINDING_REQUIRED";
|
|
2521
|
+
throw error;
|
|
2522
|
+
}
|
|
2523
|
+
const candidateBinding = await reverifyCandidateBinding(
|
|
2524
|
+
"after_colleague_contact_before_result"
|
|
2525
|
+
);
|
|
2526
|
+
if (candidateBinding?.verified !== true) {
|
|
2527
|
+
throw createRecommendDetailCandidateBindingError(candidateBinding);
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
candidate_binding: candidateBinding,
|
|
2531
|
+
skip_reason: getColleagueContactSkipReason(colleagueContact)
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
export function resolveEffectiveRecommendDetailLimit({
|
|
2536
|
+
detailLimit = Number.POSITIVE_INFINITY,
|
|
2537
|
+
postActionEnabled = false,
|
|
2538
|
+
requireColleagueContactInspection = false
|
|
2539
|
+
} = {}) {
|
|
2540
|
+
if (postActionEnabled || requireColleagueContactInspection) return Number.POSITIVE_INFINITY;
|
|
2541
|
+
return detailLimit;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
function createUnverifiedColleagueContactSkipScreening(candidate, colleagueContact) {
|
|
2545
|
+
return {
|
|
2546
|
+
status: "skip",
|
|
2547
|
+
passed: false,
|
|
2548
|
+
score: 0,
|
|
2549
|
+
reasons: ["colleague_contact_unverified"],
|
|
2550
|
+
reason: "Candidate colleague-contact status could not be verified",
|
|
2551
|
+
colleague_contact: colleagueContact,
|
|
2552
|
+
candidate
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
1237
2555
|
|
|
1238
2556
|
export async function runRecommendWorkflow({
|
|
1239
2557
|
client,
|
|
@@ -1263,10 +2581,13 @@ export async function runRecommendWorkflow({
|
|
|
1263
2581
|
postAction = "none",
|
|
1264
2582
|
maxGreetCount = null,
|
|
1265
2583
|
executePostAction = true,
|
|
1266
|
-
actionTimeoutMs = 8000,
|
|
1267
|
-
actionIntervalMs = 500,
|
|
1268
|
-
actionAfterClickDelayMs = 900,
|
|
1269
|
-
|
|
2584
|
+
actionTimeoutMs = 8000,
|
|
2585
|
+
actionIntervalMs = 500,
|
|
2586
|
+
actionAfterClickDelayMs = 900,
|
|
2587
|
+
actionJournal = null,
|
|
2588
|
+
actionJournalScope = "boss-recommend:default",
|
|
2589
|
+
reverifyActionJournalScope = null,
|
|
2590
|
+
screeningMode = "llm",
|
|
1270
2591
|
llmConfig = null,
|
|
1271
2592
|
llmTimeoutMs = 120000,
|
|
1272
2593
|
llmImageLimit = 8,
|
|
@@ -1310,14 +2631,25 @@ export async function runRecommendWorkflow({
|
|
|
1310
2631
|
const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
|
|
1311
2632
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
1312
2633
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1313
|
-
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
1314
|
-
const postActionEnabled = normalizedPostAction !== "none";
|
|
2634
|
+
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
2635
|
+
const postActionEnabled = normalizedPostAction !== "none";
|
|
2636
|
+
const screeningOnlyBindingEnabled = Boolean(
|
|
2637
|
+
normalizedPostAction === "none"
|
|
2638
|
+
&& executePostAction === false
|
|
2639
|
+
);
|
|
2640
|
+
const effectiveActionJournal = postActionEnabled && executePostAction
|
|
2641
|
+
? actionJournal || createRecommendGreetingActionJournal()
|
|
2642
|
+
: actionJournal;
|
|
1315
2643
|
const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
|
|
1316
2644
|
const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
|
|
1317
2645
|
const colleagueContactReferenceDate = new Date();
|
|
1318
|
-
const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
|
|
1319
|
-
const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
|
|
1320
|
-
const effectiveDetailLimit =
|
|
2646
|
+
const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
|
|
2647
|
+
const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
|
|
2648
|
+
const effectiveDetailLimit = resolveEffectiveRecommendDetailLimit({
|
|
2649
|
+
detailLimit: detailCountLimit,
|
|
2650
|
+
postActionEnabled,
|
|
2651
|
+
requireColleagueContactInspection: shouldSkipRecentColleagueContacted
|
|
2652
|
+
});
|
|
1321
2653
|
const networkRecorder = effectiveDetailLimit > 0
|
|
1322
2654
|
? createRecommendDetailNetworkRecorder(client)
|
|
1323
2655
|
: null;
|
|
@@ -1362,6 +2694,7 @@ export async function runRecommendWorkflow({
|
|
|
1362
2694
|
const listReadStaleDiagnostics = [];
|
|
1363
2695
|
let domStaleEventCount = 0;
|
|
1364
2696
|
let currentDomOperation = "recommend:initialize";
|
|
2697
|
+
let currentDetailCandidateBinding = null;
|
|
1365
2698
|
const domLifecycleTimeline = [];
|
|
1366
2699
|
const domStaleForensics = [];
|
|
1367
2700
|
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
@@ -1581,9 +2914,10 @@ export async function runRecommendWorkflow({
|
|
|
1581
2914
|
in_progress_candidate: {
|
|
1582
2915
|
index,
|
|
1583
2916
|
key: candidateKey,
|
|
1584
|
-
card_node_id: cardNodeId,
|
|
1585
|
-
detail_step: detailStep || null,
|
|
1586
|
-
|
|
2917
|
+
card_node_id: cardNodeId,
|
|
2918
|
+
detail_step: detailStep || null,
|
|
2919
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
2920
|
+
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
1587
2921
|
error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
|
|
1588
2922
|
},
|
|
1589
2923
|
candidate_list: compactInfiniteListState(listState)
|
|
@@ -1624,11 +2958,12 @@ export async function runRecommendWorkflow({
|
|
|
1624
2958
|
buttonSettleMs: refreshButtonSettleMs,
|
|
1625
2959
|
reloadSettleMs: refreshReloadSettleMs
|
|
1626
2960
|
});
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
2961
|
+
const verifiedExhaustion = isVerifiedRecommendRefreshCompletion(refreshResult);
|
|
2962
|
+
let blockingPanelClose = null;
|
|
2963
|
+
if (refreshResult.ok && !verifiedExhaustion) {
|
|
2964
|
+
blockingPanelClose = await closeRecommendBlockingPanels(client, {
|
|
2965
|
+
attemptsLimit: 2,
|
|
2966
|
+
rootState: refreshResult.root_state || rootState
|
|
1632
2967
|
});
|
|
1633
2968
|
}
|
|
1634
2969
|
const compactRefresh = {
|
|
@@ -1650,7 +2985,7 @@ export async function runRecommendWorkflow({
|
|
|
1650
2985
|
},
|
|
1651
2986
|
candidate_list: compactInfiniteListState(listState)
|
|
1652
2987
|
});
|
|
1653
|
-
if (!refreshResult.ok) {
|
|
2988
|
+
if (!refreshResult.ok) {
|
|
1654
2989
|
updateRecommendProgress({
|
|
1655
2990
|
refresh_method: refreshResult.method || null,
|
|
1656
2991
|
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
@@ -1662,15 +2997,56 @@ export async function runRecommendWorkflow({
|
|
|
1662
2997
|
recoveryError.code = "RECOMMEND_CONTEXT_RECOVERY_FAILED";
|
|
1663
2998
|
recoveryError.refresh_attempt = compactRefresh;
|
|
1664
2999
|
recoveryError.recovery_reason = reason;
|
|
1665
|
-
throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
|
|
1666
|
-
}
|
|
1667
|
-
if (refreshResult.
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
3000
|
+
throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
|
|
3001
|
+
}
|
|
3002
|
+
if (refreshResult.exhausted && !verifiedExhaustion) {
|
|
3003
|
+
updateRecommendProgress({
|
|
3004
|
+
refresh_method: refreshResult.method || null,
|
|
3005
|
+
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
3006
|
+
recovery_reason: reason,
|
|
3007
|
+
refresh_exhausted_unverified: true
|
|
3008
|
+
});
|
|
3009
|
+
const recoveryError = new Error(
|
|
3010
|
+
`Recommend context recovery returned unverified exhaustion after ${reason}`
|
|
3011
|
+
);
|
|
3012
|
+
recoveryError.code = "RECOMMEND_CONTEXT_RECOVERY_FAILED";
|
|
3013
|
+
recoveryError.refresh_attempt = compactRefresh;
|
|
3014
|
+
recoveryError.recovery_reason = reason;
|
|
3015
|
+
throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
|
|
3016
|
+
}
|
|
3017
|
+
if (refreshResult.current_city_only) {
|
|
3018
|
+
currentCityOnlyResult = refreshResult.current_city_only;
|
|
3019
|
+
}
|
|
3020
|
+
if (refreshResult.filter) {
|
|
3021
|
+
filterResult = refreshResult.filter;
|
|
3022
|
+
}
|
|
3023
|
+
if (verifiedExhaustion) {
|
|
3024
|
+
rootState = refreshResult.root_state || rootState;
|
|
3025
|
+
cardNodeIds = [];
|
|
3026
|
+
listEndReason = "filtered_list_exhausted";
|
|
3027
|
+
updateRecommendProgress({
|
|
3028
|
+
card_count: 0,
|
|
3029
|
+
refresh_method: refreshResult.method || null,
|
|
3030
|
+
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
3031
|
+
recovery_reason: reason,
|
|
3032
|
+
list_end_reason: listEndReason,
|
|
3033
|
+
refresh_exhausted: true,
|
|
3034
|
+
empty_state_verified: true
|
|
3035
|
+
});
|
|
3036
|
+
runControl.checkpoint({
|
|
3037
|
+
context_recovery: {
|
|
3038
|
+
attempt: contextRecoveryAttempts,
|
|
3039
|
+
reason,
|
|
3040
|
+
refresh: compactRefresh,
|
|
3041
|
+
refresh_exhausted: true,
|
|
3042
|
+
list_end_reason: listEndReason,
|
|
3043
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
3044
|
+
},
|
|
3045
|
+
candidate_list: compactInfiniteListState(listState)
|
|
3046
|
+
});
|
|
3047
|
+
return refreshResult;
|
|
3048
|
+
}
|
|
3049
|
+
if (!blockingPanelClose?.closed) {
|
|
1674
3050
|
const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
1675
3051
|
panelError.refresh_attempt = compactRefresh;
|
|
1676
3052
|
throw panelError;
|
|
@@ -1716,19 +3092,20 @@ export async function runRecommendWorkflow({
|
|
|
1716
3092
|
iframe_document_node_id: rootState.iframe.documentNodeId
|
|
1717
3093
|
});
|
|
1718
3094
|
|
|
1719
|
-
if (jobLabel) {
|
|
1720
|
-
await runControl.waitIfPaused();
|
|
1721
|
-
runControl.throwIfCanceled();
|
|
1722
|
-
runControl.setPhase("recommend:job");
|
|
1723
|
-
|
|
1724
|
-
jobLabel,
|
|
1725
|
-
settleMs: cardTimeoutMs > 45000 ? 12000 : 6000
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
rootState = await
|
|
3095
|
+
if (jobLabel) {
|
|
3096
|
+
await runControl.waitIfPaused();
|
|
3097
|
+
runControl.throwIfCanceled();
|
|
3098
|
+
runControl.setPhase("recommend:job");
|
|
3099
|
+
const initialJobSelection = await selectAndVerifyInitialRecommendJob(client, rootState, {
|
|
3100
|
+
jobLabel,
|
|
3101
|
+
settleMs: cardTimeoutMs > 45000 ? 12000 : 6000,
|
|
3102
|
+
dropdownTimeoutMs: Math.max(8000, Math.min(cardTimeoutMs, 60000)),
|
|
3103
|
+
totalTimeoutMs: Math.max(30000, Math.min(cardTimeoutMs + 15000, 90000)),
|
|
3104
|
+
retryDelayMs: 1000
|
|
3105
|
+
});
|
|
3106
|
+
jobSelection = initialJobSelection.job_selection;
|
|
3107
|
+
rootState = initialJobSelection.root_state || await getRecommendRoots(client);
|
|
3108
|
+
rootState = await ensureRecommendViewport(rootState, "job");
|
|
1732
3109
|
runControl.checkpoint({
|
|
1733
3110
|
job_selection: compactJobSelection(jobSelection)
|
|
1734
3111
|
});
|
|
@@ -1773,14 +3150,17 @@ export async function runRecommendWorkflow({
|
|
|
1773
3150
|
await runControl.waitIfPaused();
|
|
1774
3151
|
runControl.throwIfCanceled();
|
|
1775
3152
|
runControl.setPhase("recommend:filter");
|
|
1776
|
-
const result = await selectAndConfirmFirstSafeFilter(
|
|
1777
|
-
client,
|
|
1778
|
-
rootState.iframe.documentNodeId,
|
|
1779
|
-
buildRecommendFilterSelectionOptions(normalizedFilter)
|
|
1780
|
-
);
|
|
1781
|
-
if (!
|
|
1782
|
-
|
|
1783
|
-
|
|
3153
|
+
const result = await selectAndConfirmFirstSafeFilter(
|
|
3154
|
+
client,
|
|
3155
|
+
rootState.iframe.documentNodeId,
|
|
3156
|
+
buildRecommendFilterSelectionOptions(normalizedFilter)
|
|
3157
|
+
);
|
|
3158
|
+
if (!isVerifiedRecommendFilterApplication(
|
|
3159
|
+
result,
|
|
3160
|
+
buildRecommendFilterSelectionOptions(normalizedFilter)
|
|
3161
|
+
)) {
|
|
3162
|
+
throw new Error("Recommend run filter selection was not fully verified");
|
|
3163
|
+
}
|
|
1784
3164
|
rootState = await getRecommendRoots(client);
|
|
1785
3165
|
rootState = await ensureRecommendViewport(rootState, "filter");
|
|
1786
3166
|
runControl.checkpoint({
|
|
@@ -1796,19 +3176,50 @@ export async function runRecommendWorkflow({
|
|
|
1796
3176
|
runControl.throwIfCanceled();
|
|
1797
3177
|
runControl.setPhase("recommend:cards");
|
|
1798
3178
|
rootState = await ensureRecommendViewport(rootState, "cards");
|
|
1799
|
-
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
1800
|
-
timeoutMs: cardTimeoutMs,
|
|
1801
|
-
intervalMs: 300
|
|
1802
|
-
});
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
3179
|
+
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
3180
|
+
timeoutMs: cardTimeoutMs,
|
|
3181
|
+
intervalMs: 300
|
|
3182
|
+
});
|
|
3183
|
+
let initialEmptyState = null;
|
|
3184
|
+
if (!cardNodeIds.length) {
|
|
3185
|
+
initialEmptyState = await inspectRecommendFilteredEmptyState(
|
|
3186
|
+
client,
|
|
3187
|
+
rootState.iframe.documentNodeId
|
|
3188
|
+
);
|
|
3189
|
+
const initialExhausted = isVerifiedRecommendRefreshExhaustion({
|
|
3190
|
+
cardCount: 0,
|
|
3191
|
+
filter: normalizedFilter,
|
|
3192
|
+
filterResult,
|
|
3193
|
+
pageScopeResult: pageScopeSelection,
|
|
3194
|
+
currentCityOnlyResult,
|
|
3195
|
+
emptyState: initialEmptyState
|
|
3196
|
+
});
|
|
3197
|
+
if (!initialExhausted) {
|
|
3198
|
+
const error = new Error("No recommend candidate cards found for run service");
|
|
3199
|
+
error.code = "RECOMMEND_INITIAL_LIST_EMPTY_UNVERIFIED";
|
|
3200
|
+
error.empty_state = compactRecommendFilteredEmptyState(initialEmptyState);
|
|
3201
|
+
throw error;
|
|
3202
|
+
}
|
|
3203
|
+
listEndReason = "filtered_list_exhausted";
|
|
3204
|
+
runControl.checkpoint({
|
|
3205
|
+
initial_list_exhausted: true,
|
|
3206
|
+
list_end_reason: listEndReason,
|
|
3207
|
+
empty_state: compactRecommendFilteredEmptyState(initialEmptyState),
|
|
3208
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
updateRecommendProgress({
|
|
3213
|
+
card_count: cardNodeIds.length,
|
|
3214
|
+
list_end_reason: listEndReason || null,
|
|
3215
|
+
initial_list_exhausted: listEndReason === "filtered_list_exhausted",
|
|
3216
|
+
empty_state_verified: initialEmptyState?.verified === true
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
while (
|
|
3220
|
+
countPassedResults(results) < targetPassCount
|
|
3221
|
+
&& listEndReason !== "filtered_list_exhausted"
|
|
3222
|
+
) {
|
|
1812
3223
|
const candidateStarted = Date.now();
|
|
1813
3224
|
const timings = {};
|
|
1814
3225
|
await runControl.waitIfPaused();
|
|
@@ -1856,11 +3267,18 @@ export async function runRecommendWorkflow({
|
|
|
1856
3267
|
}
|
|
1857
3268
|
const listReadAcquisition = await acquireRecommendListReadWithStaleRecovery({
|
|
1858
3269
|
maxRetries: 2,
|
|
1859
|
-
acquire: async () => {
|
|
1860
|
-
await runControl.waitIfPaused();
|
|
1861
|
-
runControl.throwIfCanceled();
|
|
1862
|
-
runControl.setPhase("recommend:list-read");
|
|
1863
|
-
|
|
3270
|
+
acquire: async () => {
|
|
3271
|
+
await runControl.waitIfPaused();
|
|
3272
|
+
runControl.throwIfCanceled();
|
|
3273
|
+
runControl.setPhase("recommend:list-read");
|
|
3274
|
+
if (listEndReason === "filtered_list_exhausted") {
|
|
3275
|
+
return {
|
|
3276
|
+
ok: false,
|
|
3277
|
+
end_reached: true,
|
|
3278
|
+
reason: listEndReason
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
1864
3282
|
if (debugForcedListEnd) {
|
|
1865
3283
|
return {
|
|
1866
3284
|
ok: false,
|
|
@@ -1968,43 +3386,15 @@ export async function runRecommendWorkflow({
|
|
|
1968
3386
|
},
|
|
1969
3387
|
recover: async ({ error, diagnostic }) => {
|
|
1970
3388
|
try {
|
|
1971
|
-
return await recoverRecommendListReadStaleContext({
|
|
1972
|
-
staleAttempt: diagnostic.attempt,
|
|
1973
|
-
listState,
|
|
1974
|
-
|
|
1975
|
-
await runControl.waitIfPaused();
|
|
1976
|
-
runControl.throwIfCanceled();
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
freshRootState,
|
|
1981
|
-
"list_read_stale_root_reacquire"
|
|
1982
|
-
);
|
|
1983
|
-
const freshCardNodeIds = await waitForRecommendCardNodeIds(
|
|
1984
|
-
client,
|
|
1985
|
-
freshRootState.iframe.documentNodeId,
|
|
1986
|
-
{
|
|
1987
|
-
timeoutMs: Math.min(cardTimeoutMs, 10000),
|
|
1988
|
-
intervalMs: 300
|
|
1989
|
-
}
|
|
1990
|
-
);
|
|
1991
|
-
rootState = freshRootState;
|
|
1992
|
-
cardNodeIds = freshCardNodeIds;
|
|
1993
|
-
return {
|
|
1994
|
-
card_count: freshCardNodeIds.length,
|
|
1995
|
-
iframe_document_node_id: freshRootState.iframe.documentNodeId,
|
|
1996
|
-
root_identity: compactRecommendDomRootIdentity(
|
|
1997
|
-
freshRootState,
|
|
1998
|
-
currentConnectionEpoch()
|
|
1999
|
-
)
|
|
2000
|
-
};
|
|
2001
|
-
},
|
|
2002
|
-
contextReapply: async ({ rootReacquireError }) => {
|
|
2003
|
-
await runControl.waitIfPaused();
|
|
2004
|
-
runControl.throwIfCanceled();
|
|
2005
|
-
const recovery = await recoverAndReapplyRecommendContext(
|
|
2006
|
-
"list_read_stale_node",
|
|
2007
|
-
rootReacquireError || error,
|
|
3389
|
+
return await recoverRecommendListReadStaleContext({
|
|
3390
|
+
staleAttempt: diagnostic.attempt,
|
|
3391
|
+
listState,
|
|
3392
|
+
contextReapply: async () => {
|
|
3393
|
+
await runControl.waitIfPaused();
|
|
3394
|
+
runControl.throwIfCanceled();
|
|
3395
|
+
const recovery = await recoverAndReapplyRecommendContext(
|
|
3396
|
+
"list_read_stale_node",
|
|
3397
|
+
error,
|
|
2008
3398
|
{ forceRecentNotView: true }
|
|
2009
3399
|
);
|
|
2010
3400
|
return {
|
|
@@ -2109,10 +3499,20 @@ export async function runRecommendWorkflow({
|
|
|
2109
3499
|
});
|
|
2110
3500
|
}
|
|
2111
3501
|
});
|
|
2112
|
-
const nextCandidateResult = listReadAcquisition.result;
|
|
2113
|
-
if (!nextCandidateResult.ok) {
|
|
2114
|
-
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
2115
|
-
if (
|
|
3502
|
+
const nextCandidateResult = listReadAcquisition.result;
|
|
3503
|
+
if (!nextCandidateResult.ok) {
|
|
3504
|
+
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
3505
|
+
if (listEndReason === "filtered_list_exhausted") {
|
|
3506
|
+
cardNodeIds = [];
|
|
3507
|
+
updateRecommendProgress({
|
|
3508
|
+
card_count: 0,
|
|
3509
|
+
list_end_reason: listEndReason,
|
|
3510
|
+
refresh_exhausted: true,
|
|
3511
|
+
empty_state_verified: true
|
|
3512
|
+
});
|
|
3513
|
+
break;
|
|
3514
|
+
}
|
|
3515
|
+
if (
|
|
2116
3516
|
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
2117
3517
|
&& refreshOnEnd
|
|
2118
3518
|
&& countPassedResults(results) < targetPassCount
|
|
@@ -2147,16 +3547,49 @@ export async function runRecommendWorkflow({
|
|
|
2147
3547
|
}
|
|
2148
3548
|
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
2149
3549
|
refreshAttempts.push(compactRefresh);
|
|
2150
|
-
runControl.checkpoint({
|
|
2151
|
-
refresh_round: refreshRounds,
|
|
2152
|
-
refresh: compactRefresh
|
|
2153
|
-
});
|
|
2154
|
-
updateRecommendProgress({
|
|
2155
|
-
card_count: refreshResult.card_count
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3550
|
+
runControl.checkpoint({
|
|
3551
|
+
refresh_round: refreshRounds,
|
|
3552
|
+
refresh: compactRefresh
|
|
3553
|
+
});
|
|
3554
|
+
updateRecommendProgress({
|
|
3555
|
+
card_count: Number.isInteger(refreshResult.card_count)
|
|
3556
|
+
? refreshResult.card_count
|
|
3557
|
+
: cardNodeIds.length,
|
|
3558
|
+
refresh_method: refreshResult.method || null,
|
|
3559
|
+
refresh_forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view),
|
|
3560
|
+
list_end_reason: listEndReason
|
|
3561
|
+
});
|
|
3562
|
+
if (refreshResult.exhausted && !isVerifiedRecommendRefreshCompletion(refreshResult)) {
|
|
3563
|
+
throw createRecommendRefreshFailureError(compactRefresh, {
|
|
3564
|
+
listEndReason,
|
|
3565
|
+
targetCount: targetPassCount,
|
|
3566
|
+
passedCount: countPassedResults(results)
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
if (isVerifiedRecommendRefreshCompletion(refreshResult)) {
|
|
3570
|
+
if (refreshResult.current_city_only) {
|
|
3571
|
+
currentCityOnlyResult = refreshResult.current_city_only;
|
|
3572
|
+
}
|
|
3573
|
+
if (refreshResult.filter) {
|
|
3574
|
+
filterResult = refreshResult.filter;
|
|
3575
|
+
}
|
|
3576
|
+
cardNodeIds = [];
|
|
3577
|
+
listEndReason = "filtered_list_exhausted";
|
|
3578
|
+
updateRecommendProgress({
|
|
3579
|
+
card_count: 0,
|
|
3580
|
+
list_end_reason: listEndReason,
|
|
3581
|
+
refresh_exhausted: true,
|
|
3582
|
+
empty_state_verified: true
|
|
3583
|
+
});
|
|
3584
|
+
runControl.checkpoint({
|
|
3585
|
+
refresh_round: refreshRounds,
|
|
3586
|
+
refresh: compactRefresh,
|
|
3587
|
+
refresh_exhausted: true,
|
|
3588
|
+
list_end_reason: listEndReason,
|
|
3589
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
3590
|
+
});
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
2160
3593
|
if (refreshResult.ok) {
|
|
2161
3594
|
if (refreshResult.current_city_only) {
|
|
2162
3595
|
currentCityOnlyResult = refreshResult.current_city_only;
|
|
@@ -2208,12 +3641,241 @@ export async function runRecommendWorkflow({
|
|
|
2208
3641
|
let cardCandidate = nextCandidateResult.item.candidate;
|
|
2209
3642
|
|
|
2210
3643
|
let screeningCandidate = cardCandidate;
|
|
2211
|
-
let detailResult = null;
|
|
2212
|
-
let recoverableDetailError = null;
|
|
2213
|
-
let
|
|
2214
|
-
let
|
|
2215
|
-
let
|
|
2216
|
-
|
|
3644
|
+
let detailResult = null;
|
|
3645
|
+
let recoverableDetailError = null;
|
|
3646
|
+
let candidateLocalDetailFailurePending = false;
|
|
3647
|
+
let candidateLocalDetailTerminalError = null;
|
|
3648
|
+
let colleagueContact = null;
|
|
3649
|
+
let colleagueContactSkipReason = "";
|
|
3650
|
+
let detailStep = "not_started";
|
|
3651
|
+
let openedDetailState = null;
|
|
3652
|
+
currentDetailCandidateBinding = null;
|
|
3653
|
+
|
|
3654
|
+
async function requireCurrentDetailCandidateBinding(stage, {
|
|
3655
|
+
allowScroll = true,
|
|
3656
|
+
settleMs = 120
|
|
3657
|
+
} = {}) {
|
|
3658
|
+
const bindingContext = openedDetailState?.candidate_binding_context || {
|
|
3659
|
+
card_pre_click_provenance: currentDetailCandidateBinding?.card?.pre_click_provenance || null,
|
|
3660
|
+
detail_roots_before: currentDetailCandidateBinding?.detail?.roots_before_capture || null,
|
|
3661
|
+
expected_detail_root: currentDetailCandidateBinding?.detail?.root || null,
|
|
3662
|
+
allow_card_disappearance: true,
|
|
3663
|
+
card_click_evidence: currentDetailCandidateBinding?.card?.click_evidence || null,
|
|
3664
|
+
click_attempts: currentDetailCandidateBinding?.card?.click_attempts || []
|
|
3665
|
+
};
|
|
3666
|
+
const freshDetailState = await waitForRecommendDetail(client, {
|
|
3667
|
+
timeoutMs: 4000,
|
|
3668
|
+
intervalMs: 200
|
|
3669
|
+
});
|
|
3670
|
+
if (!freshDetailState?.popup && !freshDetailState?.resumeIframe) {
|
|
3671
|
+
throw createRecommendDetailCandidateBindingError({
|
|
3672
|
+
schema_version: 1,
|
|
3673
|
+
verified: false,
|
|
3674
|
+
reason: "detail_not_visible_during_reverification",
|
|
3675
|
+
method: null,
|
|
3676
|
+
expected_candidate_id: cardCandidate?.id || null,
|
|
3677
|
+
expected_name: cardCandidate?.identity?.name || null,
|
|
3678
|
+
card: currentDetailCandidateBinding?.card || null,
|
|
3679
|
+
detail: null
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
openedDetailState = {
|
|
3683
|
+
...freshDetailState,
|
|
3684
|
+
candidate_binding: currentDetailCandidateBinding,
|
|
3685
|
+
candidate_binding_context: bindingContext
|
|
3686
|
+
};
|
|
3687
|
+
const binding = await verifyRecommendDetailCandidateBinding(client, {
|
|
3688
|
+
cardNodeId,
|
|
3689
|
+
cardCandidate,
|
|
3690
|
+
detailState: freshDetailState,
|
|
3691
|
+
cardEvidenceBefore: bindingContext?.card_pre_click_provenance?.card || null,
|
|
3692
|
+
cardPreClickProvenance: bindingContext?.card_pre_click_provenance || null,
|
|
3693
|
+
detailRootsBefore: bindingContext?.detail_roots_before || null,
|
|
3694
|
+
// In zero-outbound screening mode Boss may replace the short-lived
|
|
3695
|
+
// loading dialog with the final resume popup. Re-prove a stable CV
|
|
3696
|
+
// target in the fresh popup instead of requiring the loading root's
|
|
3697
|
+
// backend identity to survive that legitimate replacement.
|
|
3698
|
+
expectedDetailRoot: screeningOnlyBindingEnabled
|
|
3699
|
+
? null
|
|
3700
|
+
: bindingContext?.expected_detail_root || null,
|
|
3701
|
+
allowCardDisappearance: bindingContext?.allow_card_disappearance === true,
|
|
3702
|
+
cardClickEvidence: bindingContext?.card_click_evidence || null,
|
|
3703
|
+
clickAttempts: Array.isArray(bindingContext?.click_attempts)
|
|
3704
|
+
? bindingContext.click_attempts
|
|
3705
|
+
: [],
|
|
3706
|
+
settleMs: allowScroll ? Math.max(0, Number(settleMs) || 0) : 0,
|
|
3707
|
+
allowScroll: allowScroll === true
|
|
3708
|
+
});
|
|
3709
|
+
currentDetailCandidateBinding = binding;
|
|
3710
|
+
if (openedDetailState) {
|
|
3711
|
+
openedDetailState = {
|
|
3712
|
+
...openedDetailState,
|
|
3713
|
+
candidate_binding: binding,
|
|
3714
|
+
candidate_binding_context: bindingContext
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
if (detailResult) detailResult.candidate_binding = binding;
|
|
3718
|
+
checkpointInProgressCandidate({
|
|
3719
|
+
index,
|
|
3720
|
+
candidateKey,
|
|
3721
|
+
cardNodeId,
|
|
3722
|
+
detailStep: stage || detailStep
|
|
3723
|
+
});
|
|
3724
|
+
const acceptedCandidateBinding = Boolean(
|
|
3725
|
+
binding.verified === true
|
|
3726
|
+
|| (
|
|
3727
|
+
screeningOnlyBindingEnabled
|
|
3728
|
+
&& binding.screening_verified === true
|
|
3729
|
+
)
|
|
3730
|
+
);
|
|
3731
|
+
if (!acceptedCandidateBinding) {
|
|
3732
|
+
throw createRecommendDetailCandidateBindingError(binding);
|
|
3733
|
+
}
|
|
3734
|
+
return binding;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
async function containCandidateLocalDetailBindingFailure(error, {
|
|
3738
|
+
phase = "recommend:detail-binding",
|
|
3739
|
+
operation = currentDomOperation,
|
|
3740
|
+
forensic = null
|
|
3741
|
+
} = {}) {
|
|
3742
|
+
if (candidateLocalDetailFailurePending) return;
|
|
3743
|
+
if (error?.detail_candidate_binding) {
|
|
3744
|
+
currentDetailCandidateBinding = error.detail_candidate_binding;
|
|
3745
|
+
}
|
|
3746
|
+
currentDetailCandidateBinding = preserveRecommendDetailCandidateBindingForRecovery(
|
|
3747
|
+
detailResult,
|
|
3748
|
+
currentDetailCandidateBinding
|
|
3749
|
+
);
|
|
3750
|
+
candidateLocalDetailFailurePending = true;
|
|
3751
|
+
const localForensic = forensic || checkpointDomStaleForensic(error, {
|
|
3752
|
+
phase,
|
|
3753
|
+
operation,
|
|
3754
|
+
detailStep,
|
|
3755
|
+
candidateIndex: index,
|
|
3756
|
+
candidateKey,
|
|
3757
|
+
cardNodeId
|
|
3758
|
+
});
|
|
3759
|
+
checkpointDomStaleRecovery(localForensic, {
|
|
3760
|
+
status: "candidate_failed_closed_no_context_recovery"
|
|
3761
|
+
});
|
|
3762
|
+
recoverableDetailError = error;
|
|
3763
|
+
detailResult = null;
|
|
3764
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
3765
|
+
|
|
3766
|
+
let detailCleanup = null;
|
|
3767
|
+
let detailCleanupError = null;
|
|
3768
|
+
try {
|
|
3769
|
+
detailCleanup = await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
3770
|
+
} catch (cleanupError) {
|
|
3771
|
+
detailCleanupError = cleanupError;
|
|
3772
|
+
}
|
|
3773
|
+
let avatarCleanup = null;
|
|
3774
|
+
let avatarCleanupError = null;
|
|
3775
|
+
try {
|
|
3776
|
+
avatarCleanup = await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
|
|
3777
|
+
} catch (cleanupError) {
|
|
3778
|
+
avatarCleanupError = cleanupError;
|
|
3779
|
+
}
|
|
3780
|
+
let cleanupRootState = null;
|
|
3781
|
+
let cleanupRootError = null;
|
|
3782
|
+
try {
|
|
3783
|
+
cleanupRootState = await getRecommendRoots(client);
|
|
3784
|
+
cleanupRootState = await ensureRecommendViewport(
|
|
3785
|
+
cleanupRootState,
|
|
3786
|
+
"detail_binding_candidate_local_cleanup"
|
|
3787
|
+
);
|
|
3788
|
+
} catch (cleanupError) {
|
|
3789
|
+
cleanupRootError = cleanupError;
|
|
3790
|
+
}
|
|
3791
|
+
let panelCleanup = null;
|
|
3792
|
+
let panelCleanupError = null;
|
|
3793
|
+
try {
|
|
3794
|
+
panelCleanup = await closeRecommendBlockingPanels(client, {
|
|
3795
|
+
attemptsLimit: 2,
|
|
3796
|
+
rootState: cleanupRootState || rootState
|
|
3797
|
+
});
|
|
3798
|
+
} catch (cleanupError) {
|
|
3799
|
+
panelCleanupError = cleanupError;
|
|
3800
|
+
}
|
|
3801
|
+
timings.detail_candidate_local_cleanup = {
|
|
3802
|
+
detail: compactCloseResult(detailCleanup),
|
|
3803
|
+
detail_error: detailCleanupError?.message || null,
|
|
3804
|
+
avatar_preview: compactCloseResult(avatarCleanup),
|
|
3805
|
+
avatar_preview_error: avatarCleanupError?.message || null,
|
|
3806
|
+
blocking_panels: compactCloseResult(panelCleanup),
|
|
3807
|
+
blocking_panels_error: panelCleanupError?.message || null,
|
|
3808
|
+
root_reacquired: Boolean(cleanupRootState?.iframe?.documentNodeId),
|
|
3809
|
+
root_error: cleanupRootError?.message || null,
|
|
3810
|
+
card_count: null
|
|
3811
|
+
};
|
|
3812
|
+
if (
|
|
3813
|
+
detailCleanup?.closed !== true
|
|
3814
|
+
|| avatarCleanup?.closed !== true
|
|
3815
|
+
|| panelCleanup?.closed !== true
|
|
3816
|
+
|| detailCleanupError
|
|
3817
|
+
|| avatarCleanupError
|
|
3818
|
+
|| panelCleanupError
|
|
3819
|
+
|| cleanupRootError
|
|
3820
|
+
|| !cleanupRootState?.iframe?.documentNodeId
|
|
3821
|
+
) {
|
|
3822
|
+
const cleanupError = new Error(
|
|
3823
|
+
"Recommend candidate-local detail binding cleanup did not restore a closed page state"
|
|
3824
|
+
);
|
|
3825
|
+
cleanupError.code = "RECOMMEND_DETAIL_LOCAL_CLEANUP_FAILED";
|
|
3826
|
+
cleanupError.phase = "recommend:detail-binding-cleanup";
|
|
3827
|
+
cleanupError.cleanup = timings.detail_candidate_local_cleanup;
|
|
3828
|
+
cleanupError.detail_candidate_binding = currentDetailCandidateBinding;
|
|
3829
|
+
candidateLocalDetailTerminalError = cleanupError;
|
|
3830
|
+
checkpointInProgressCandidate({
|
|
3831
|
+
index,
|
|
3832
|
+
candidateKey,
|
|
3833
|
+
cardNodeId,
|
|
3834
|
+
detailStep: "detail_binding_cleanup_failed",
|
|
3835
|
+
error: cleanupError
|
|
3836
|
+
});
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
try {
|
|
3841
|
+
const freshCardNodeIds = await waitForRecommendCardNodeIds(
|
|
3842
|
+
client,
|
|
3843
|
+
cleanupRootState.iframe.documentNodeId,
|
|
3844
|
+
{
|
|
3845
|
+
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
3846
|
+
intervalMs: 300
|
|
3847
|
+
}
|
|
3848
|
+
);
|
|
3849
|
+
if (!Array.isArray(freshCardNodeIds) || freshCardNodeIds.length === 0) {
|
|
3850
|
+
throw new Error("Recommend candidate-local cleanup reacquired no candidate cards");
|
|
3851
|
+
}
|
|
3852
|
+
rootState = cleanupRootState;
|
|
3853
|
+
cardNodeIds = freshCardNodeIds;
|
|
3854
|
+
timings.detail_candidate_local_cleanup.card_count = freshCardNodeIds.length;
|
|
3855
|
+
} catch (reacquireError) {
|
|
3856
|
+
const cleanupError = new Error(
|
|
3857
|
+
"Recommend candidate-local detail cleanup could not reacquire the candidate list"
|
|
3858
|
+
);
|
|
3859
|
+
cleanupError.code = "RECOMMEND_DETAIL_LOCAL_CLEANUP_FAILED";
|
|
3860
|
+
cleanupError.phase = "recommend:detail-binding-cleanup";
|
|
3861
|
+
cleanupError.cause = reacquireError;
|
|
3862
|
+
cleanupError.cleanup = {
|
|
3863
|
+
...timings.detail_candidate_local_cleanup,
|
|
3864
|
+
reacquire_error: reacquireError?.message || String(reacquireError)
|
|
3865
|
+
};
|
|
3866
|
+
cleanupError.detail_candidate_binding = currentDetailCandidateBinding;
|
|
3867
|
+
candidateLocalDetailTerminalError = cleanupError;
|
|
3868
|
+
checkpointInProgressCandidate({
|
|
3869
|
+
index,
|
|
3870
|
+
candidateKey,
|
|
3871
|
+
cardNodeId,
|
|
3872
|
+
detailStep: "detail_binding_list_reacquire_failed",
|
|
3873
|
+
error: cleanupError
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
if (index < effectiveDetailLimit) {
|
|
2217
3879
|
try {
|
|
2218
3880
|
await runControl.waitIfPaused();
|
|
2219
3881
|
runControl.throwIfCanceled();
|
|
@@ -2247,65 +3909,93 @@ export async function runRecommendWorkflow({
|
|
|
2247
3909
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2248
3910
|
networkRecorder.clear();
|
|
2249
3911
|
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
2250
|
-
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
3912
|
+
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
2251
3913
|
cardNodeId,
|
|
2252
3914
|
candidateKey,
|
|
2253
3915
|
cardCandidate,
|
|
2254
3916
|
rootState,
|
|
2255
|
-
targetUrl,
|
|
2256
|
-
retryTimeoutMs: 8000,
|
|
2257
|
-
|
|
2258
|
-
|
|
3917
|
+
targetUrl,
|
|
3918
|
+
retryTimeoutMs: 8000,
|
|
3919
|
+
acceptScreeningBinding: screeningOnlyBindingEnabled,
|
|
3920
|
+
maxAttempts: 3
|
|
3921
|
+
});
|
|
2259
3922
|
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
2260
3923
|
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
2261
|
-
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
2262
|
-
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
2263
|
-
screeningCandidate = cardCandidate;
|
|
3924
|
+
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
3925
|
+
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
3926
|
+
screeningCandidate = cardCandidate;
|
|
3927
|
+
openedDetailState = openedDetail.detail_state;
|
|
3928
|
+
currentDetailCandidateBinding = openedDetail.candidate_binding || openedDetail.detail_state?.candidate_binding || null;
|
|
3929
|
+
checkpointInProgressCandidate({
|
|
3930
|
+
index,
|
|
3931
|
+
candidateKey,
|
|
3932
|
+
cardNodeId,
|
|
3933
|
+
detailStep: "detail_binding_verified"
|
|
3934
|
+
});
|
|
2264
3935
|
if (shouldSkipRecentColleagueContacted) {
|
|
2265
3936
|
detailStep = "check_colleague_contact";
|
|
2266
3937
|
currentDomOperation = "candidate:detail-colleague-contact";
|
|
3938
|
+
await requireCurrentDetailCandidateBinding("before_colleague_contact");
|
|
2267
3939
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2268
3940
|
try {
|
|
2269
3941
|
colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
|
|
2270
3942
|
client,
|
|
2271
|
-
|
|
3943
|
+
openedDetailState,
|
|
2272
3944
|
{
|
|
2273
3945
|
referenceDate: colleagueContactReferenceDate,
|
|
2274
3946
|
windowDays: normalizedColleagueContactWindowDays
|
|
2275
3947
|
}
|
|
2276
3948
|
));
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
colleagueContact = {
|
|
3951
|
+
checked: false,
|
|
3952
|
+
panel_found: false,
|
|
3953
|
+
recent: null,
|
|
3954
|
+
indeterminate: true,
|
|
3955
|
+
reason: "inspection_failed",
|
|
3956
|
+
error: error?.message || String(error),
|
|
3957
|
+
window_days: normalizedColleagueContactWindowDays
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
detailStep = "verify_after_colleague_contact";
|
|
3961
|
+
currentDomOperation = "candidate:detail-verify-after-colleague-contact";
|
|
3962
|
+
const boundColleagueResult = await bindRecommendColleagueContactInspectionResult(
|
|
3963
|
+
colleagueContact,
|
|
3964
|
+
{ reverifyCandidateBinding: requireCurrentDetailCandidateBinding }
|
|
3965
|
+
);
|
|
3966
|
+
currentDetailCandidateBinding = boundColleagueResult.candidate_binding;
|
|
3967
|
+
colleagueContactSkipReason = boundColleagueResult.skip_reason;
|
|
3968
|
+
if (colleagueContactSkipReason) {
|
|
3969
|
+
detailStep = "colleague_contact_skip_result";
|
|
3970
|
+
currentDomOperation = "candidate:detail-colleague-contact-skip-result";
|
|
3971
|
+
await requireCurrentDetailCandidateBinding("before_colleague_contact_skip_result");
|
|
3972
|
+
detailResult = {
|
|
3973
|
+
candidate: screeningCandidate,
|
|
3974
|
+
candidate_binding: currentDetailCandidateBinding,
|
|
3975
|
+
detail: {
|
|
3976
|
+
popup_text: "",
|
|
3977
|
+
resume_text: ""
|
|
3978
|
+
},
|
|
3979
|
+
colleague_contact: colleagueContact,
|
|
3980
|
+
cv_acquisition: {
|
|
3981
|
+
source: colleagueContactSkipReason,
|
|
3982
|
+
skipped: true,
|
|
3983
|
+
reason: colleagueContactSkipReason
|
|
3984
|
+
}
|
|
3985
|
+
};
|
|
3986
|
+
detailStep = "colleague_contact_skip_close";
|
|
3987
|
+
currentDomOperation = "candidate:detail-colleague-contact-skip-close";
|
|
3988
|
+
await requireCurrentDetailCandidateBinding("before_colleague_contact_skip_close");
|
|
3989
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
3990
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
3991
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
if (!colleagueContactSkipReason) {
|
|
2306
3995
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
2307
3996
|
detailStep = "wait_network";
|
|
2308
3997
|
currentDomOperation = "candidate:detail-network-wait";
|
|
3998
|
+
await requireCurrentDetailCandidateBinding("before_cv_network_wait");
|
|
2309
3999
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2310
4000
|
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
2311
4001
|
waitForRecommendDetailNetworkEvents,
|
|
@@ -2322,11 +4012,12 @@ export async function runRecommendWorkflow({
|
|
|
2322
4012
|
}
|
|
2323
4013
|
detailStep = "extract_detail";
|
|
2324
4014
|
currentDomOperation = "candidate:detail-extract";
|
|
4015
|
+
await requireCurrentDetailCandidateBinding("before_cv_extract");
|
|
2325
4016
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2326
|
-
detailResult = await extractRecommendDetailCandidate(client, {
|
|
4017
|
+
detailResult = await extractRecommendDetailCandidate(client, {
|
|
2327
4018
|
cardCandidate,
|
|
2328
4019
|
cardNodeId,
|
|
2329
|
-
detailState:
|
|
4020
|
+
detailState: openedDetailState,
|
|
2330
4021
|
networkEvents: networkRecorder.events,
|
|
2331
4022
|
targetUrl,
|
|
2332
4023
|
closeDetail: false,
|
|
@@ -2349,8 +4040,9 @@ export async function runRecommendWorkflow({
|
|
|
2349
4040
|
} else {
|
|
2350
4041
|
detailStep = "wait_capture_target";
|
|
2351
4042
|
currentDomOperation = "candidate:detail-wait-capture-target";
|
|
4043
|
+
await requireCurrentDetailCandidateBinding("before_cv_capture_target");
|
|
2352
4044
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2353
|
-
captureTargetWait = await waitForCvCaptureTarget(client,
|
|
4045
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetailState, {
|
|
2354
4046
|
domain: "recommend",
|
|
2355
4047
|
timeoutMs: 6000,
|
|
2356
4048
|
intervalMs: 250
|
|
@@ -2404,10 +4096,38 @@ export async function runRecommendWorkflow({
|
|
|
2404
4096
|
}
|
|
2405
4097
|
}
|
|
2406
4098
|
);
|
|
4099
|
+
const reacquireCaptureTargetAfterBinding = async ({
|
|
4100
|
+
timeoutMs = 4000,
|
|
4101
|
+
intervalMs = 200
|
|
4102
|
+
} = {}) => {
|
|
4103
|
+
const reboundWait = await waitForCvCaptureTarget(client, openedDetailState, {
|
|
4104
|
+
domain: "recommend",
|
|
4105
|
+
timeoutMs,
|
|
4106
|
+
intervalMs
|
|
4107
|
+
});
|
|
4108
|
+
const reboundTarget = reboundWait?.target || null;
|
|
4109
|
+
if (!reboundTarget?.node_id) {
|
|
4110
|
+
const error = new Error(
|
|
4111
|
+
"Recommend CV capture target was unavailable after binding re-verification"
|
|
4112
|
+
);
|
|
4113
|
+
error.code = "IMAGE_CAPTURE_TARGET_UNAVAILABLE";
|
|
4114
|
+
error.capture_target_wait = reboundWait || null;
|
|
4115
|
+
throw error;
|
|
4116
|
+
}
|
|
4117
|
+
captureTargetWait = reboundWait;
|
|
4118
|
+
captureTarget = reboundTarget;
|
|
4119
|
+
return reboundTarget;
|
|
4120
|
+
};
|
|
2407
4121
|
try {
|
|
2408
4122
|
detailStep = "capture_image";
|
|
2409
4123
|
currentDomOperation = "candidate:detail-capture-image";
|
|
4124
|
+
await requireCurrentDetailCandidateBinding("before_cv_image_capture");
|
|
2410
4125
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
4126
|
+
// Binding re-verification obtains a fresh document tree and can
|
|
4127
|
+
// invalidate frontend node ids returned by the earlier target
|
|
4128
|
+
// discovery. Reacquire the contained CV node after that proof,
|
|
4129
|
+
// then capture it without any intervening DOM-root read.
|
|
4130
|
+
await reacquireCaptureTargetAfterBinding();
|
|
2411
4131
|
imageEvidence = await measureTiming(
|
|
2412
4132
|
timings,
|
|
2413
4133
|
"screenshot_capture_ms",
|
|
@@ -2449,11 +4169,15 @@ export async function runRecommendWorkflow({
|
|
|
2449
4169
|
intervalMs: 200
|
|
2450
4170
|
})
|
|
2451
4171
|
}),
|
|
2452
|
-
capture: (reacquired) => {
|
|
4172
|
+
capture: async (reacquired) => {
|
|
2453
4173
|
rootState = reacquired.root_state;
|
|
4174
|
+
openedDetailState = reacquired.detail_state;
|
|
2454
4175
|
captureTarget = reacquired.target;
|
|
2455
4176
|
captureTargetWait = reacquired.target_wait;
|
|
2456
4177
|
detailStep = "resume_image_capture";
|
|
4178
|
+
currentDomOperation = "candidate:detail-resume-image-capture";
|
|
4179
|
+
await requireCurrentDetailCandidateBinding("before_resumed_cv_image_capture");
|
|
4180
|
+
await reacquireCaptureTargetAfterBinding();
|
|
2457
4181
|
return measureTiming(
|
|
2458
4182
|
timings,
|
|
2459
4183
|
"screenshot_capture_ms",
|
|
@@ -2586,19 +4310,32 @@ export async function runRecommendWorkflow({
|
|
|
2586
4310
|
}
|
|
2587
4311
|
|
|
2588
4312
|
detailResult.image_evidence = imageEvidence;
|
|
2589
|
-
detailResult.cv_acquisition = {
|
|
4313
|
+
detailResult.cv_acquisition = {
|
|
2590
4314
|
source,
|
|
2591
4315
|
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
2592
4316
|
wait_plan: waitPlan,
|
|
2593
|
-
network_wait: networkWait,
|
|
2594
|
-
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
2595
|
-
|
|
4317
|
+
network_wait: networkWait,
|
|
4318
|
+
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
4319
|
+
network_profile_binding: detailResult.network_profile_binding || null,
|
|
4320
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
2596
4321
|
capture_target: captureTarget || null,
|
|
2597
|
-
capture_target_wait: captureTargetWait
|
|
2598
|
-
};
|
|
2599
|
-
|
|
2600
|
-
|
|
4322
|
+
capture_target_wait: captureTargetWait
|
|
4323
|
+
};
|
|
4324
|
+
detailStep = "verify_before_screening";
|
|
4325
|
+
currentDomOperation = "candidate:detail-verify-before-screening";
|
|
4326
|
+
await requireCurrentDetailCandidateBinding("before_llm_or_local_screening");
|
|
4327
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
4328
|
+
screeningCandidate = detailResult.candidate;
|
|
4329
|
+
detailResult.screening_candidate_identity = assertRecommendScreeningCandidateMatchesCard({
|
|
4330
|
+
cardCandidate,
|
|
4331
|
+
screeningCandidate,
|
|
4332
|
+
stage: "before_screening_candidate_assignment"
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
2601
4335
|
} catch (error) {
|
|
4336
|
+
if (error?.detail_candidate_binding) {
|
|
4337
|
+
currentDetailCandidateBinding = error.detail_candidate_binding;
|
|
4338
|
+
}
|
|
2602
4339
|
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
2603
4340
|
const staleForensic = checkpointDomStaleForensic(error, {
|
|
2604
4341
|
phase: "recommend:detail",
|
|
@@ -2608,54 +4345,81 @@ export async function runRecommendWorkflow({
|
|
|
2608
4345
|
candidateKey,
|
|
2609
4346
|
cardNodeId
|
|
2610
4347
|
});
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
4348
|
+
currentDetailCandidateBinding = preserveRecommendDetailCandidateBindingForRecovery(
|
|
4349
|
+
detailResult,
|
|
4350
|
+
currentDetailCandidateBinding
|
|
4351
|
+
);
|
|
4352
|
+
const failureDisposition = getRecommendDetailFailureDisposition(error);
|
|
4353
|
+
if (failureDisposition.candidate_local) {
|
|
4354
|
+
await containCandidateLocalDetailBindingFailure(error, {
|
|
4355
|
+
phase: "recommend:detail",
|
|
4356
|
+
operation: currentDomOperation,
|
|
4357
|
+
forensic: staleForensic
|
|
4358
|
+
});
|
|
4359
|
+
} else {
|
|
4360
|
+
const recoveryReservation = reserveRecommendDetailRecovery(
|
|
4361
|
+
candidateRecoveryCounts,
|
|
4362
|
+
candidateKey,
|
|
4363
|
+
1
|
|
4364
|
+
);
|
|
4365
|
+
if (recoveryReservation.allowed) {
|
|
4366
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
4367
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
4368
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
4369
|
+
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
|
|
4370
|
+
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
4371
|
+
try {
|
|
4372
|
+
const recovery = await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
|
|
4373
|
+
forceRecentNotView: true
|
|
4374
|
+
});
|
|
4375
|
+
checkpointDomStaleRecovery(staleForensic, {
|
|
4376
|
+
status: "recovered",
|
|
4377
|
+
recoveryResult: recovery
|
|
4378
|
+
});
|
|
4379
|
+
} catch (recoveryError) {
|
|
4380
|
+
checkpointDomStaleRecovery(staleForensic, {
|
|
4381
|
+
status: "recovery_failed",
|
|
4382
|
+
recoveryError
|
|
4383
|
+
});
|
|
4384
|
+
throw recoveryError;
|
|
4385
|
+
}
|
|
4386
|
+
continue;
|
|
4387
|
+
}
|
|
4388
|
+
checkpointDomStaleRecovery(staleForensic, { status: "candidate_failed_closed" });
|
|
4389
|
+
recoverableDetailError = error;
|
|
4390
|
+
detailResult = null;
|
|
4391
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
2616
4392
|
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2617
4393
|
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2618
4394
|
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
2619
|
-
try {
|
|
2620
|
-
const recovery = await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
|
|
2621
|
-
forceRecentNotView: true
|
|
2622
|
-
});
|
|
2623
|
-
checkpointDomStaleRecovery(staleForensic, {
|
|
2624
|
-
status: "recovered",
|
|
2625
|
-
recoveryResult: recovery
|
|
2626
|
-
});
|
|
2627
|
-
} catch (recoveryError) {
|
|
2628
|
-
checkpointDomStaleRecovery(staleForensic, {
|
|
2629
|
-
status: "recovery_failed",
|
|
2630
|
-
recoveryError
|
|
2631
|
-
});
|
|
2632
|
-
throw recoveryError;
|
|
2633
|
-
}
|
|
2634
|
-
continue;
|
|
2635
4395
|
}
|
|
2636
|
-
|
|
2637
|
-
recoverableDetailError = error;
|
|
2638
|
-
detailResult = null;
|
|
2639
|
-
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
2640
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2641
|
-
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2642
|
-
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
2643
|
-
}
|
|
4396
|
+
}
|
|
2644
4397
|
}
|
|
2645
4398
|
|
|
2646
|
-
|
|
2647
|
-
|
|
4399
|
+
if (!candidateLocalDetailFailurePending) {
|
|
4400
|
+
await runControl.waitIfPaused();
|
|
4401
|
+
runControl.throwIfCanceled();
|
|
4402
|
+
}
|
|
2648
4403
|
runControl.setPhase("recommend:screening");
|
|
2649
4404
|
let llmResult = null;
|
|
2650
4405
|
const imageAcquisitionFailed = shouldFailClosedRecommendImageAcquisition(detailResult);
|
|
2651
4406
|
if (useLlmScreening) {
|
|
2652
|
-
if (
|
|
4407
|
+
if (colleagueContactSkipReason || recoverableDetailError || imageAcquisitionFailed) {
|
|
2653
4408
|
llmResult = null;
|
|
2654
|
-
} else if (!llmConfig) {
|
|
2655
|
-
llmResult = createMissingLlmConfigResult();
|
|
2656
|
-
} else {
|
|
2657
|
-
try {
|
|
2658
|
-
|
|
4409
|
+
} else if (!llmConfig) {
|
|
4410
|
+
llmResult = createMissingLlmConfigResult();
|
|
4411
|
+
} else {
|
|
4412
|
+
try {
|
|
4413
|
+
detailStep = "llm_screening_binding";
|
|
4414
|
+
currentDomOperation = "candidate:detail-verify-before-llm";
|
|
4415
|
+
await requireCurrentDetailCandidateBinding("immediately_before_llm_screening");
|
|
4416
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
4417
|
+
detailResult.screening_candidate_identity = assertRecommendScreeningCandidateMatchesCard({
|
|
4418
|
+
cardCandidate,
|
|
4419
|
+
screeningCandidate,
|
|
4420
|
+
stage: "immediately_before_llm_screening"
|
|
4421
|
+
});
|
|
4422
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
2659
4423
|
? "vision_model_ms"
|
|
2660
4424
|
: "text_model_ms";
|
|
2661
4425
|
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
@@ -2666,22 +4430,33 @@ export async function runRecommendWorkflow({
|
|
|
2666
4430
|
imageEvidence: detailResult?.image_evidence || null,
|
|
2667
4431
|
maxImages: llmImageLimit,
|
|
2668
4432
|
imageDetail: llmImageDetail
|
|
2669
|
-
}));
|
|
2670
|
-
} catch (error) {
|
|
2671
|
-
if (
|
|
2672
|
-
throw
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
4433
|
+
}));
|
|
4434
|
+
} catch (error) {
|
|
4435
|
+
if (error?.code === "RECOMMEND_SCREENING_CANDIDATE_IDENTITY_MISMATCH") {
|
|
4436
|
+
throw error;
|
|
4437
|
+
} else if (isRecommendDetailCandidateBindingError(error)) {
|
|
4438
|
+
await containCandidateLocalDetailBindingFailure(error, {
|
|
4439
|
+
phase: "recommend:llm-binding",
|
|
4440
|
+
operation: currentDomOperation
|
|
4441
|
+
});
|
|
4442
|
+
llmResult = null;
|
|
4443
|
+
} else if (isFatalLlmProviderError(error)) {
|
|
4444
|
+
throw createFatalLlmRunError(error, {
|
|
4445
|
+
domain: "recommend",
|
|
4446
|
+
candidate: screeningCandidate
|
|
4447
|
+
});
|
|
4448
|
+
} else {
|
|
4449
|
+
llmResult = createFailedLlmScreeningResult(error);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
2679
4452
|
}
|
|
2680
4453
|
if (detailResult) detailResult.llm_result = llmResult;
|
|
2681
4454
|
}
|
|
2682
|
-
|
|
2683
|
-
? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
|
|
2684
|
-
:
|
|
4455
|
+
let screening = colleagueContactSkipReason === "skipped_recent_colleague_contact"
|
|
4456
|
+
? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
|
|
4457
|
+
: colleagueContactSkipReason === "colleague_contact_unverified"
|
|
4458
|
+
? createUnverifiedColleagueContactSkipScreening(screeningCandidate, colleagueContact)
|
|
4459
|
+
: recoverableDetailError
|
|
2685
4460
|
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
2686
4461
|
: imageAcquisitionFailed
|
|
2687
4462
|
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
@@ -2695,7 +4470,12 @@ export async function runRecommendWorkflow({
|
|
|
2695
4470
|
let postActionResult = null;
|
|
2696
4471
|
let closeFailureError = null;
|
|
2697
4472
|
let closeRecoveryFailure = null;
|
|
2698
|
-
if (
|
|
4473
|
+
if (
|
|
4474
|
+
postActionEnabled
|
|
4475
|
+
&& screening?.passed === true
|
|
4476
|
+
&& detailResult
|
|
4477
|
+
&& !colleagueContactSkipReason
|
|
4478
|
+
) {
|
|
2699
4479
|
try {
|
|
2700
4480
|
const postActionStarted = Date.now();
|
|
2701
4481
|
await runControl.waitIfPaused();
|
|
@@ -2703,6 +4483,8 @@ export async function runRecommendWorkflow({
|
|
|
2703
4483
|
runControl.setPhase("recommend:post-action");
|
|
2704
4484
|
detailStep = "post_action_discovery";
|
|
2705
4485
|
currentDomOperation = "candidate:post-action-discovery";
|
|
4486
|
+
await requireCurrentDetailCandidateBinding("before_post_action_discovery");
|
|
4487
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
2706
4488
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2707
4489
|
await maybeHumanActionCooldown("before_post_action", timings);
|
|
2708
4490
|
actionDiscovery = await waitForRecommendDetailActionControls(client, {
|
|
@@ -2712,6 +4494,8 @@ export async function runRecommendWorkflow({
|
|
|
2712
4494
|
});
|
|
2713
4495
|
detailStep = "post_action_execute";
|
|
2714
4496
|
currentDomOperation = "candidate:post-action-execute";
|
|
4497
|
+
await requireCurrentDetailCandidateBinding("before_post_action_execute");
|
|
4498
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
2715
4499
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2716
4500
|
postActionResult = await runRecommendPostAction({
|
|
2717
4501
|
client,
|
|
@@ -2721,14 +4505,27 @@ export async function runRecommendWorkflow({
|
|
|
2721
4505
|
greetCount,
|
|
2722
4506
|
maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
2723
4507
|
executePostAction,
|
|
2724
|
-
afterClickDelayMs: actionAfterClickDelayMs
|
|
4508
|
+
afterClickDelayMs: actionAfterClickDelayMs,
|
|
4509
|
+
candidateId: screeningCandidate.id || cardCandidate.id || "",
|
|
4510
|
+
candidateBinding: currentDetailCandidateBinding,
|
|
4511
|
+
reverifyCandidateBinding: async (stage, options = {}) => {
|
|
4512
|
+
await requireCurrentDetailCandidateBinding(stage, options);
|
|
4513
|
+
detailResult.candidate_binding = currentDetailCandidateBinding;
|
|
4514
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep: stage });
|
|
4515
|
+
return currentDetailCandidateBinding;
|
|
4516
|
+
},
|
|
4517
|
+
actionJournal: effectiveActionJournal,
|
|
4518
|
+
actionJournalScope,
|
|
4519
|
+
reverifyActionJournalScope,
|
|
4520
|
+
runId: runControl.runId,
|
|
4521
|
+
checkpointCritical: (patch) => runControl.checkpointCritical(patch)
|
|
2725
4522
|
});
|
|
2726
4523
|
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
2727
4524
|
greetCount += 1;
|
|
2728
4525
|
}
|
|
2729
4526
|
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
2730
4527
|
} catch (error) {
|
|
2731
|
-
checkpointDomStaleForensic(error, {
|
|
4528
|
+
const postActionForensic = checkpointDomStaleForensic(error, {
|
|
2732
4529
|
phase: "recommend:post-action",
|
|
2733
4530
|
operation: currentDomOperation,
|
|
2734
4531
|
detailStep,
|
|
@@ -2736,9 +4533,150 @@ export async function runRecommendWorkflow({
|
|
|
2736
4533
|
candidateKey,
|
|
2737
4534
|
cardNodeId
|
|
2738
4535
|
});
|
|
2739
|
-
|
|
4536
|
+
if (isRecommendDetailCandidateBindingError(error)) {
|
|
4537
|
+
await containCandidateLocalDetailBindingFailure(error, {
|
|
4538
|
+
phase: "recommend:post-action-binding",
|
|
4539
|
+
operation: currentDomOperation,
|
|
4540
|
+
forensic: postActionForensic
|
|
4541
|
+
});
|
|
4542
|
+
screening = createRecoverableDetailFailureScreening(screeningCandidate, error);
|
|
4543
|
+
actionDiscovery = null;
|
|
4544
|
+
postActionResult = null;
|
|
4545
|
+
} else {
|
|
4546
|
+
throw error;
|
|
4547
|
+
}
|
|
2740
4548
|
}
|
|
2741
4549
|
}
|
|
4550
|
+
if (postActionResult?.stop_run || postActionResult?.outcome_unknown) {
|
|
4551
|
+
const preservePostInputDetail = postActionResult?.preserve_detail_on_terminal === true;
|
|
4552
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
4553
|
+
const stopResult = {
|
|
4554
|
+
index,
|
|
4555
|
+
candidate_key: candidateKey,
|
|
4556
|
+
card_node_id: cardNodeId,
|
|
4557
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4558
|
+
candidate: compactCandidate(screeningCandidate),
|
|
4559
|
+
detail: compactDetail(detailResult),
|
|
4560
|
+
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
4561
|
+
screening: compactScreening(screening),
|
|
4562
|
+
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
4563
|
+
post_action: postActionResult,
|
|
4564
|
+
error: recoverableDetailError
|
|
4565
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
4566
|
+
: detailResult?.image_evidence?.ok === false
|
|
4567
|
+
? compactError({
|
|
4568
|
+
code: detailResult.image_evidence.error_code,
|
|
4569
|
+
message: detailResult.image_evidence.error
|
|
4570
|
+
}, "IMAGE_CAPTURE_FAILED")
|
|
4571
|
+
: null,
|
|
4572
|
+
timings
|
|
4573
|
+
};
|
|
4574
|
+
results.push(stopResult);
|
|
4575
|
+
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
4576
|
+
metadata: {
|
|
4577
|
+
result_index: index,
|
|
4578
|
+
candidate_id: screeningCandidate.id || null
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
4581
|
+
updateRecommendProgress({
|
|
4582
|
+
last_candidate_id: screeningCandidate.id || null,
|
|
4583
|
+
last_candidate_key: candidateKey,
|
|
4584
|
+
last_score: screening.score
|
|
4585
|
+
});
|
|
4586
|
+
const terminalCandidateCheckpoint = {
|
|
4587
|
+
in_progress_candidate: null,
|
|
4588
|
+
results,
|
|
4589
|
+
preserve_detail_on_terminal: preservePostInputDetail,
|
|
4590
|
+
action_result_critical_persisted: !preservePostInputDetail,
|
|
4591
|
+
terminal_preservation: preservePostInputDetail
|
|
4592
|
+
? postActionResult.terminal_preservation || {
|
|
4593
|
+
required: true,
|
|
4594
|
+
reason: "post_input_action_journal_persistence_failed",
|
|
4595
|
+
candidate_id: screeningCandidate.id || null,
|
|
4596
|
+
action_state: postActionResult?.action_transaction?.state || null,
|
|
4597
|
+
result_index: index
|
|
4598
|
+
}
|
|
4599
|
+
: null,
|
|
4600
|
+
last_candidate: {
|
|
4601
|
+
id: screeningCandidate.id || null,
|
|
4602
|
+
key: candidateKey,
|
|
4603
|
+
identity: screeningCandidate.identity || {},
|
|
4604
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4605
|
+
screening: {
|
|
4606
|
+
status: screening.status,
|
|
4607
|
+
passed: screening.passed,
|
|
4608
|
+
score: screening.score
|
|
4609
|
+
},
|
|
4610
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
4611
|
+
error: stopResult.error,
|
|
4612
|
+
post_action: postActionResult
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
checkpointRecommendPostActionStopResult(runControl, terminalCandidateCheckpoint, {
|
|
4616
|
+
candidateResult: stopResult,
|
|
4617
|
+
candidateId: screeningCandidate.id || "",
|
|
4618
|
+
actionState: postActionResult?.action_transaction?.state || null,
|
|
4619
|
+
resultIndex: index
|
|
4620
|
+
});
|
|
4621
|
+
listEndReason = postActionResult.reason || "post_action_stop";
|
|
4622
|
+
break;
|
|
4623
|
+
}
|
|
4624
|
+
if (
|
|
4625
|
+
postActionResult?.action_clicked === true
|
|
4626
|
+
&& postActionResult?.action_transaction?.state === "greeting_confirmed"
|
|
4627
|
+
) {
|
|
4628
|
+
const durablePostActionResult = {
|
|
4629
|
+
index,
|
|
4630
|
+
candidate_key: candidateKey,
|
|
4631
|
+
card_node_id: cardNodeId,
|
|
4632
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4633
|
+
candidate: compactCandidate(screeningCandidate),
|
|
4634
|
+
detail: compactDetail(detailResult),
|
|
4635
|
+
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
4636
|
+
screening: compactScreening(screening),
|
|
4637
|
+
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
4638
|
+
post_action: postActionResult,
|
|
4639
|
+
error: recoverableDetailError
|
|
4640
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
4641
|
+
: detailResult?.image_evidence?.ok === false
|
|
4642
|
+
? compactError({
|
|
4643
|
+
code: detailResult.image_evidence.error_code,
|
|
4644
|
+
message: detailResult.image_evidence.error
|
|
4645
|
+
}, "IMAGE_CAPTURE_FAILED")
|
|
4646
|
+
: null,
|
|
4647
|
+
timings: {
|
|
4648
|
+
...timings,
|
|
4649
|
+
total_ms: Date.now() - candidateStarted
|
|
4650
|
+
},
|
|
4651
|
+
provisional_before_detail_cleanup: true
|
|
4652
|
+
};
|
|
4653
|
+
checkpointRecommendPostActionStopResult(runControl, {
|
|
4654
|
+
in_progress_candidate: null,
|
|
4655
|
+
results: [...results, durablePostActionResult],
|
|
4656
|
+
preserve_detail_on_terminal: false,
|
|
4657
|
+
action_result_critical_persisted: true,
|
|
4658
|
+
post_action_candidate_result_persisted: true,
|
|
4659
|
+
last_candidate: {
|
|
4660
|
+
id: screeningCandidate.id || null,
|
|
4661
|
+
key: candidateKey,
|
|
4662
|
+
identity: screeningCandidate.identity || {},
|
|
4663
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4664
|
+
screening: {
|
|
4665
|
+
status: screening.status,
|
|
4666
|
+
passed: screening.passed,
|
|
4667
|
+
score: screening.score
|
|
4668
|
+
},
|
|
4669
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
4670
|
+
error: durablePostActionResult.error,
|
|
4671
|
+
post_action: postActionResult
|
|
4672
|
+
}
|
|
4673
|
+
}, {
|
|
4674
|
+
candidateResult: durablePostActionResult,
|
|
4675
|
+
candidateId: screeningCandidate.id || "",
|
|
4676
|
+
actionState: "greeting_confirmed",
|
|
4677
|
+
resultIndex: index
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
2742
4680
|
if (detailResult && closeDetail && !detailResult.close_result?.closed) {
|
|
2743
4681
|
runControl.setPhase("recommend:close-detail");
|
|
2744
4682
|
detailStep = "close_detail";
|
|
@@ -2794,9 +4732,10 @@ export async function runRecommendWorkflow({
|
|
|
2794
4732
|
timings.total_ms = Date.now() - candidateStarted;
|
|
2795
4733
|
const compactResult = {
|
|
2796
4734
|
index,
|
|
2797
|
-
candidate_key: candidateKey,
|
|
2798
|
-
card_node_id: cardNodeId,
|
|
2799
|
-
|
|
4735
|
+
candidate_key: candidateKey,
|
|
4736
|
+
card_node_id: cardNodeId,
|
|
4737
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4738
|
+
candidate: compactCandidate(screeningCandidate),
|
|
2800
4739
|
detail: compactDetail(detailResult),
|
|
2801
4740
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
2802
4741
|
screening: compactScreening(screening),
|
|
@@ -2826,15 +4765,39 @@ export async function runRecommendWorkflow({
|
|
|
2826
4765
|
last_candidate_id: screeningCandidate.id || null,
|
|
2827
4766
|
last_candidate_key: candidateKey,
|
|
2828
4767
|
last_score: screening.score
|
|
2829
|
-
});
|
|
4768
|
+
});
|
|
2830
4769
|
const checkpointStarted = Date.now();
|
|
2831
|
-
|
|
4770
|
+
const completedCandidateCheckpoint = {
|
|
2832
4771
|
in_progress_candidate: null,
|
|
2833
4772
|
results,
|
|
2834
|
-
|
|
4773
|
+
candidate_list: compactInfiniteListState(listState),
|
|
4774
|
+
candidate_local_detail_failure: candidateLocalDetailFailurePending
|
|
4775
|
+
? {
|
|
4776
|
+
required: true,
|
|
4777
|
+
terminal: Boolean(candidateLocalDetailTerminalError),
|
|
4778
|
+
candidate_id: screeningCandidate.id || null,
|
|
4779
|
+
candidate_key: candidateKey,
|
|
4780
|
+
result_index: index,
|
|
4781
|
+
binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
4782
|
+
cleanup: timings.detail_candidate_local_cleanup || null
|
|
4783
|
+
}
|
|
4784
|
+
: null,
|
|
4785
|
+
candidate_local_detail_terminal: candidateLocalDetailTerminalError
|
|
4786
|
+
? {
|
|
4787
|
+
required: true,
|
|
4788
|
+
code: candidateLocalDetailTerminalError.code || null,
|
|
4789
|
+
phase: candidateLocalDetailTerminalError.phase || null,
|
|
4790
|
+
candidate_id: screeningCandidate.id || null,
|
|
4791
|
+
candidate_key: candidateKey,
|
|
4792
|
+
result_index: index,
|
|
4793
|
+
cleanup: candidateLocalDetailTerminalError.cleanup || null
|
|
4794
|
+
}
|
|
4795
|
+
: null,
|
|
4796
|
+
last_candidate: {
|
|
2835
4797
|
id: screeningCandidate.id || null,
|
|
2836
|
-
key: candidateKey,
|
|
2837
|
-
identity: screeningCandidate.identity || {},
|
|
4798
|
+
key: candidateKey,
|
|
4799
|
+
identity: screeningCandidate.identity || {},
|
|
4800
|
+
candidate_binding: compactRecommendDetailCandidateBinding(currentDetailCandidateBinding),
|
|
2838
4801
|
screening: {
|
|
2839
4802
|
status: screening.status,
|
|
2840
4803
|
passed: screening.passed,
|
|
@@ -2842,12 +4805,28 @@ export async function runRecommendWorkflow({
|
|
|
2842
4805
|
},
|
|
2843
4806
|
llm_screening: compactScreeningLlmResult(llmResult),
|
|
2844
4807
|
error: compactResult.error,
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
4808
|
+
post_action: postActionResult
|
|
4809
|
+
}
|
|
4810
|
+
};
|
|
4811
|
+
if (candidateLocalDetailFailurePending) {
|
|
4812
|
+
runControl.checkpointCritical(completedCandidateCheckpoint);
|
|
4813
|
+
} else {
|
|
4814
|
+
runControl.checkpoint(completedCandidateCheckpoint);
|
|
4815
|
+
}
|
|
4816
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
4817
|
+
|
|
4818
|
+
if (candidateLocalDetailTerminalError) {
|
|
4819
|
+
candidateLocalDetailTerminalError.candidate_result_persisted = true;
|
|
4820
|
+
candidateLocalDetailTerminalError.candidate_result_index = index;
|
|
4821
|
+
throw candidateLocalDetailTerminalError;
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4824
|
+
if (candidateLocalDetailFailurePending) {
|
|
4825
|
+
await runControl.waitIfPaused();
|
|
4826
|
+
runControl.throwIfCanceled();
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
if (closeRecoveryFailure) {
|
|
2851
4830
|
throw closeRecoveryFailure;
|
|
2852
4831
|
}
|
|
2853
4832
|
|
|
@@ -2921,10 +4900,33 @@ export async function runRecommendWorkflow({
|
|
|
2921
4900
|
transient_recovered: countRecommendResultStatuses(results, { greetCount }).transient_recovered
|
|
2922
4901
|
+ listReadStaleRecoveries,
|
|
2923
4902
|
results
|
|
2924
|
-
};
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
function createRecommendPostActionTerminalFailure(summary = null) {
|
|
4907
|
+
const results = Array.isArray(summary?.results) ? summary.results : [];
|
|
4908
|
+
const stoppedResult = [...results].reverse().find((result) => (
|
|
4909
|
+
result?.post_action?.stop_run === true
|
|
4910
|
+
)) || null;
|
|
4911
|
+
const postAction = stoppedResult?.post_action || null;
|
|
4912
|
+
if (!postAction) return null;
|
|
4913
|
+
if (
|
|
4914
|
+
postAction.out_of_greet_credits === true
|
|
4915
|
+
|| postAction.reason === "greet_credits_exhausted"
|
|
4916
|
+
) return null;
|
|
4917
|
+
const reason = String(postAction.reason || summary?.list_end_reason || "unknown").trim();
|
|
4918
|
+
const error = new Error(`Recommend post-action terminal failure: ${reason}`);
|
|
4919
|
+
error.code = postAction.outcome_unknown === true
|
|
4920
|
+
? "RECOMMEND_GREET_OUTCOME_UNKNOWN"
|
|
4921
|
+
: postAction.pre_input_aborted === true
|
|
4922
|
+
? "RECOMMEND_GREET_PRE_INPUT_ABORTED"
|
|
4923
|
+
: "RECOMMEND_POST_ACTION_TERMINAL_FAILURE";
|
|
4924
|
+
error.phase = "recommend:post-action-terminal";
|
|
4925
|
+
error.run_summary = summary;
|
|
4926
|
+
return error;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
export function createRecommendRunService({
|
|
2928
4930
|
lifecycle,
|
|
2929
4931
|
idPrefix = "recommend",
|
|
2930
4932
|
workflow = runRecommendWorkflow,
|
|
@@ -2962,9 +4964,12 @@ export function createRecommendRunService({
|
|
|
2962
4964
|
postAction = "none",
|
|
2963
4965
|
maxGreetCount = null,
|
|
2964
4966
|
executePostAction = true,
|
|
2965
|
-
actionTimeoutMs = 8000,
|
|
2966
|
-
actionIntervalMs = 500,
|
|
2967
|
-
actionAfterClickDelayMs = 900,
|
|
4967
|
+
actionTimeoutMs = 8000,
|
|
4968
|
+
actionIntervalMs = 500,
|
|
4969
|
+
actionAfterClickDelayMs = 900,
|
|
4970
|
+
actionJournal = null,
|
|
4971
|
+
actionJournalScope = "boss-recommend:default",
|
|
4972
|
+
reverifyActionJournalScope = null,
|
|
2968
4973
|
screeningMode = "llm",
|
|
2969
4974
|
llmConfig = null,
|
|
2970
4975
|
llmTimeoutMs = 120000,
|
|
@@ -3033,7 +5038,9 @@ export function createRecommendRunService({
|
|
|
3033
5038
|
post_action: normalizedPostAction,
|
|
3034
5039
|
max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
3035
5040
|
execute_post_action: Boolean(executePostAction),
|
|
3036
|
-
action_timeout_ms: actionTimeoutMs,
|
|
5041
|
+
action_timeout_ms: actionTimeoutMs,
|
|
5042
|
+
action_journal_enabled: Boolean(normalizedPostAction !== "none" && executePostAction),
|
|
5043
|
+
action_journal_scope: actionJournalScope,
|
|
3037
5044
|
screening_mode: normalizedScreeningMode,
|
|
3038
5045
|
llm_configured: Boolean(llmConfig),
|
|
3039
5046
|
llm_timeout_ms: llmTimeoutMs,
|
|
@@ -3086,53 +5093,61 @@ export function createRecommendRunService({
|
|
|
3086
5093
|
debug_boundary_trigger_count: 0
|
|
3087
5094
|
},
|
|
3088
5095
|
checkpoint: {},
|
|
3089
|
-
task: (runControl) =>
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
5096
|
+
task: async (runControl) => {
|
|
5097
|
+
const summary = await workflow({
|
|
5098
|
+
client,
|
|
5099
|
+
targetUrl,
|
|
5100
|
+
criteria,
|
|
5101
|
+
jobLabel,
|
|
5102
|
+
pageScope: requestedPageScope,
|
|
5103
|
+
fallbackPageScope: normalizedFallbackPageScope,
|
|
5104
|
+
filter: normalizedFilter,
|
|
5105
|
+
maxCandidates,
|
|
5106
|
+
detailLimit: normalizedDetailLimit,
|
|
5107
|
+
closeDetail,
|
|
5108
|
+
delayMs,
|
|
5109
|
+
cardTimeoutMs,
|
|
5110
|
+
maxImagePages,
|
|
5111
|
+
imageWheelDeltaY,
|
|
5112
|
+
cvAcquisitionMode,
|
|
5113
|
+
listMaxScrolls,
|
|
5114
|
+
listStableSignatureLimit,
|
|
5115
|
+
listWheelDeltaY,
|
|
5116
|
+
listSettleMs,
|
|
5117
|
+
listFallbackPoint,
|
|
5118
|
+
refreshOnEnd,
|
|
5119
|
+
maxRefreshRounds,
|
|
5120
|
+
refreshButtonSettleMs,
|
|
5121
|
+
refreshReloadSettleMs,
|
|
5122
|
+
postAction: normalizedPostAction,
|
|
5123
|
+
maxGreetCount,
|
|
5124
|
+
executePostAction,
|
|
5125
|
+
actionTimeoutMs,
|
|
5126
|
+
actionIntervalMs,
|
|
5127
|
+
actionAfterClickDelayMs,
|
|
5128
|
+
actionJournal,
|
|
5129
|
+
actionJournalScope,
|
|
5130
|
+
reverifyActionJournalScope,
|
|
5131
|
+
screeningMode: normalizedScreeningMode,
|
|
5132
|
+
llmConfig,
|
|
5133
|
+
llmTimeoutMs,
|
|
5134
|
+
llmImageLimit,
|
|
5135
|
+
llmImageDetail,
|
|
5136
|
+
imageOutputDir,
|
|
5137
|
+
humanRestEnabled: effectiveHumanRestEnabled,
|
|
5138
|
+
humanBehavior: effectiveHumanBehavior,
|
|
5139
|
+
skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
|
|
5140
|
+
colleagueContactWindowDays: normalizedColleagueContactWindowDays,
|
|
5141
|
+
debugTestMode: debugBoundaryOptions.debugTestMode,
|
|
5142
|
+
debugForceListEndAfterProcessed: debugBoundaryOptions.debugForceListEndAfterProcessed,
|
|
5143
|
+
debugForceContextRecoveryAfterProcessed: debugBoundaryOptions.debugForceContextRecoveryAfterProcessed,
|
|
5144
|
+
debugForceCdpReconnectAfterProcessed: debugBoundaryOptions.debugForceCdpReconnectAfterProcessed
|
|
5145
|
+
}, runControl);
|
|
5146
|
+
const terminalFailure = createRecommendPostActionTerminalFailure(summary);
|
|
5147
|
+
if (terminalFailure) throw terminalFailure;
|
|
5148
|
+
return summary;
|
|
5149
|
+
}
|
|
5150
|
+
});
|
|
3136
5151
|
}
|
|
3137
5152
|
|
|
3138
5153
|
return {
|