@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.24
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/README.md +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +14 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +291 -114
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +11 -2
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +112 -42
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +443 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +515 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +12 -3
- package/src/domains/recommend/filters.js +611 -611
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -736
- package/src/domains/recommend/refresh.js +410 -329
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1783 -592
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +124 -5
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +515 -80
- package/src/recommend-scheduler.js +66 -0
|
@@ -16,16 +16,25 @@ import {
|
|
|
16
16
|
sleep
|
|
17
17
|
} from "../../core/browser/index.js";
|
|
18
18
|
import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
attemptImageCaptureCheckpointResume,
|
|
21
|
+
compactCvAcquisitionState,
|
|
22
|
+
createImageCaptureWorkflowRetryTracker,
|
|
23
|
+
createRequiredImageEvidenceFailure,
|
|
24
|
+
imageCaptureResumeCheckpoint,
|
|
21
25
|
countParsedNetworkProfiles,
|
|
22
26
|
createCvAcquisitionState,
|
|
23
27
|
DEFAULT_MAX_IMAGE_PAGES,
|
|
24
|
-
getCvNetworkWaitPlan,
|
|
25
|
-
|
|
28
|
+
getCvNetworkWaitPlan,
|
|
29
|
+
isFailedClosedImageAcquisition,
|
|
30
|
+
isIncompleteImageEvidence,
|
|
31
|
+
isRecoverableImageCaptureWorkflowError,
|
|
32
|
+
recordCvImageFallback,
|
|
26
33
|
recordCvNetworkHit,
|
|
27
|
-
recordCvNetworkMiss,
|
|
28
|
-
|
|
34
|
+
recordCvNetworkMiss,
|
|
35
|
+
reacquireImageCaptureResumeTarget,
|
|
36
|
+
requireCompleteImageEvidence,
|
|
37
|
+
summarizeImageEvidence,
|
|
29
38
|
waitForCvNetworkEvents
|
|
30
39
|
} from "../../core/cv-acquisition/index.js";
|
|
31
40
|
import {
|
|
@@ -55,22 +64,23 @@ import {
|
|
|
55
64
|
extractRecommendDetailCandidate,
|
|
56
65
|
isRecommendDetailOpenMissError,
|
|
57
66
|
isStaleRecommendNodeError,
|
|
58
|
-
openRecommendCardDetailWithFreshRetry,
|
|
59
|
-
|
|
67
|
+
openRecommendCardDetailWithFreshRetry,
|
|
68
|
+
waitForRecommendDetail,
|
|
69
|
+
waitForRecommendDetailNetworkEvents
|
|
60
70
|
} from "./detail.js";
|
|
61
|
-
import {
|
|
62
|
-
readRecommendCardCandidate,
|
|
63
|
-
waitForRecommendCardNodeIds
|
|
64
|
-
} from "./cards.js";
|
|
65
|
-
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
66
|
-
import { ensureRecommendCurrentCityOnly } from "./location.js";
|
|
67
|
-
import {
|
|
68
|
-
applyRecommendFilterEnvelopeStages,
|
|
69
|
-
buildRecommendFilterSelectionOptions,
|
|
70
|
-
refreshRecommendListAtEnd
|
|
71
|
-
} from "./refresh.js";
|
|
72
|
-
import { selectRecommendJob } from "./jobs.js";
|
|
73
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
readRecommendCardCandidate,
|
|
73
|
+
waitForRecommendCardNodeIds
|
|
74
|
+
} from "./cards.js";
|
|
75
|
+
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
76
|
+
import { ensureRecommendCurrentCityOnly } from "./location.js";
|
|
77
|
+
import {
|
|
78
|
+
applyRecommendFilterEnvelopeStages,
|
|
79
|
+
buildRecommendFilterSelectionOptions,
|
|
80
|
+
refreshRecommendListAtEnd
|
|
81
|
+
} from "./refresh.js";
|
|
82
|
+
import { selectRecommendJob } from "./jobs.js";
|
|
83
|
+
import {
|
|
74
84
|
normalizeRecommendPageScope,
|
|
75
85
|
selectRecommendPageScope
|
|
76
86
|
} from "./scopes.js";
|
|
@@ -90,6 +100,427 @@ import {
|
|
|
90
100
|
} from "./actions.js";
|
|
91
101
|
import { getRecommendRoots } from "./roots.js";
|
|
92
102
|
|
|
103
|
+
const RECOMMEND_DEBUG_BOUNDARY_MODES = Object.freeze({
|
|
104
|
+
list_end: "debug_force_list_end_after_processed",
|
|
105
|
+
context_recovery: "debug_force_context_recovery_after_processed",
|
|
106
|
+
cdp_reconnect: "debug_force_cdp_reconnect_after_processed"
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function hasOwn(source, key) {
|
|
110
|
+
return Boolean(source && Object.prototype.hasOwnProperty.call(source, key));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readDebugBoundaryValue(source, snakeKey, camelKey) {
|
|
114
|
+
if (hasOwn(source, snakeKey)) return source[snakeKey];
|
|
115
|
+
if (hasOwn(source, camelKey)) return source[camelKey];
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeDebugBoundaryThreshold(raw, field) {
|
|
120
|
+
if (raw === undefined || raw === null || raw === "") return null;
|
|
121
|
+
const parsed = Number(raw);
|
|
122
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
123
|
+
const error = new Error(`${field} must be a positive integer`);
|
|
124
|
+
error.code = "INVALID_RECOMMEND_DEBUG_BOUNDARY";
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeRecommendDebugBoundaryOptions(source = {}) {
|
|
131
|
+
const debugTestMode = source.debugTestMode === true || source.debug_test_mode === true;
|
|
132
|
+
const thresholds = {
|
|
133
|
+
list_end: normalizeDebugBoundaryThreshold(
|
|
134
|
+
readDebugBoundaryValue(
|
|
135
|
+
source,
|
|
136
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.list_end,
|
|
137
|
+
"debugForceListEndAfterProcessed"
|
|
138
|
+
),
|
|
139
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.list_end
|
|
140
|
+
),
|
|
141
|
+
context_recovery: normalizeDebugBoundaryThreshold(
|
|
142
|
+
readDebugBoundaryValue(
|
|
143
|
+
source,
|
|
144
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery,
|
|
145
|
+
"debugForceContextRecoveryAfterProcessed"
|
|
146
|
+
),
|
|
147
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery
|
|
148
|
+
),
|
|
149
|
+
cdp_reconnect: normalizeDebugBoundaryThreshold(
|
|
150
|
+
readDebugBoundaryValue(
|
|
151
|
+
source,
|
|
152
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect,
|
|
153
|
+
"debugForceCdpReconnectAfterProcessed"
|
|
154
|
+
),
|
|
155
|
+
RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect
|
|
156
|
+
)
|
|
157
|
+
};
|
|
158
|
+
const configured = Object.entries(thresholds).filter(([, threshold]) => threshold !== null);
|
|
159
|
+
if (configured.length > 1) {
|
|
160
|
+
const error = new Error(
|
|
161
|
+
`${configured.map(([mode]) => RECOMMEND_DEBUG_BOUNDARY_MODES[mode]).join(", ")} are mutually exclusive`
|
|
162
|
+
);
|
|
163
|
+
error.code = "RECOMMEND_DEBUG_BOUNDARIES_MUTUALLY_EXCLUSIVE";
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (configured.length && !debugTestMode) {
|
|
167
|
+
const error = new Error(
|
|
168
|
+
`${RECOMMEND_DEBUG_BOUNDARY_MODES[configured[0][0]]} requires debug_test_mode=true`
|
|
169
|
+
);
|
|
170
|
+
error.code = "DEBUG_TEST_MODE_REQUIRED";
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
const [configuredEntry = null] = configured;
|
|
174
|
+
return {
|
|
175
|
+
debugTestMode,
|
|
176
|
+
mode: configuredEntry?.[0] || null,
|
|
177
|
+
field: configuredEntry ? RECOMMEND_DEBUG_BOUNDARY_MODES[configuredEntry[0]] : null,
|
|
178
|
+
threshold: configuredEntry?.[1] ?? null,
|
|
179
|
+
debugForceListEndAfterProcessed: thresholds.list_end,
|
|
180
|
+
debugForceContextRecoveryAfterProcessed: thresholds.context_recovery,
|
|
181
|
+
debugForceCdpReconnectAfterProcessed: thresholds.cdp_reconnect
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createRecommendDebugBoundaryController(source = {}) {
|
|
186
|
+
const config = normalizeRecommendDebugBoundaryOptions(source);
|
|
187
|
+
let triggered = false;
|
|
188
|
+
let triggerCount = 0;
|
|
189
|
+
return {
|
|
190
|
+
config,
|
|
191
|
+
take(processedCount) {
|
|
192
|
+
const processed = Math.max(0, Number(processedCount) || 0);
|
|
193
|
+
if (triggered || !config.mode || processed < config.threshold) return null;
|
|
194
|
+
triggered = true;
|
|
195
|
+
triggerCount += 1;
|
|
196
|
+
return {
|
|
197
|
+
mode: config.mode,
|
|
198
|
+
field: config.field,
|
|
199
|
+
threshold: config.threshold,
|
|
200
|
+
processed,
|
|
201
|
+
trigger_count: triggerCount
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
getState() {
|
|
205
|
+
return {
|
|
206
|
+
...config,
|
|
207
|
+
triggered,
|
|
208
|
+
trigger_count: triggerCount
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readErrorChainField(error, key) {
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
let current = error;
|
|
217
|
+
for (let depth = 0; current && depth < 6; depth += 1) {
|
|
218
|
+
if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
|
|
219
|
+
if (typeof current === "object" || typeof current === "function") seen.add(current);
|
|
220
|
+
if (current?.[key] !== undefined && current?.[key] !== null) return current[key];
|
|
221
|
+
current = current?.cause || null;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function compactCdpFailureDiagnostic(error, {
|
|
227
|
+
fallbackCode = "RECOMMEND_DOM_STALE",
|
|
228
|
+
fallbackPhase = ""
|
|
229
|
+
} = {}) {
|
|
230
|
+
const message = String(readErrorChainField(error, "message") || error || "DOM node became stale");
|
|
231
|
+
const diagnostic = {
|
|
232
|
+
name: String(readErrorChainField(error, "name") || "Error").slice(0, 100),
|
|
233
|
+
code: String(readErrorChainField(error, "code") || fallbackCode).slice(0, 160),
|
|
234
|
+
message: message.slice(0, 500),
|
|
235
|
+
phase: String(readErrorChainField(error, "phase") || fallbackPhase || "").slice(0, 200) || null,
|
|
236
|
+
cdp_method: String(readErrorChainField(error, "cdp_method") || "").slice(0, 200) || null,
|
|
237
|
+
cdp_at: String(readErrorChainField(error, "cdp_at") || "").slice(0, 100) || null,
|
|
238
|
+
cdp_search_id: String(readErrorChainField(error, "cdp_search_id") || "").slice(0, 200) || null,
|
|
239
|
+
cdp_replay_policy: String(readErrorChainField(error, "cdp_replay_policy") || "").slice(0, 100) || null,
|
|
240
|
+
cdp_reconnect_error: String(readErrorChainField(error, "cdp_reconnect_error") || "").slice(0, 300) || null
|
|
241
|
+
};
|
|
242
|
+
for (const key of [
|
|
243
|
+
"cdp_node_id",
|
|
244
|
+
"cdp_backend_node_id",
|
|
245
|
+
"cdp_connection_epoch",
|
|
246
|
+
"cdp_reconnected_epoch"
|
|
247
|
+
]) {
|
|
248
|
+
const value = Number(readErrorChainField(error, key));
|
|
249
|
+
diagnostic[key] = Number.isInteger(value) && value >= 0 ? value : null;
|
|
250
|
+
}
|
|
251
|
+
for (const key of [
|
|
252
|
+
"cdp_reconnected",
|
|
253
|
+
"cdp_replayed_after_reconnect",
|
|
254
|
+
"cdp_replay_suppressed",
|
|
255
|
+
"cdp_outcome_unknown"
|
|
256
|
+
]) {
|
|
257
|
+
const value = readErrorChainField(error, key);
|
|
258
|
+
diagnostic[key] = typeof value === "boolean" ? value : null;
|
|
259
|
+
}
|
|
260
|
+
const paramKeys = readErrorChainField(error, "cdp_param_keys");
|
|
261
|
+
diagnostic.cdp_param_keys = Array.isArray(paramKeys)
|
|
262
|
+
? paramKeys
|
|
263
|
+
.map((key) => String(key || "").trim())
|
|
264
|
+
.filter((key) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key))
|
|
265
|
+
.slice(0, 20)
|
|
266
|
+
: [];
|
|
267
|
+
return diagnostic;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function compactRecommendDomRootIdentity(rootState = null, connectionEpoch = null) {
|
|
271
|
+
const epoch = Number(connectionEpoch);
|
|
272
|
+
const topNodeId = Number(rootState?.rootNodes?.top || rootState?.topRoot?.nodeId || 0);
|
|
273
|
+
const frameOwnerNodeId = Number(rootState?.rootNodes?.frameOwner || rootState?.iframe?.nodeId || 0);
|
|
274
|
+
const frameDocumentNodeId = Number(
|
|
275
|
+
rootState?.rootNodes?.frame || rootState?.iframe?.documentNodeId || 0
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
connection_epoch: Number.isInteger(epoch) && epoch > 0 ? epoch : null,
|
|
279
|
+
top_document_node_id: Number.isInteger(topNodeId) && topNodeId > 0 ? topNodeId : null,
|
|
280
|
+
iframe_owner_node_id: Number.isInteger(frameOwnerNodeId) && frameOwnerNodeId > 0
|
|
281
|
+
? frameOwnerNodeId
|
|
282
|
+
: null,
|
|
283
|
+
iframe_document_node_id: Number.isInteger(frameDocumentNodeId) && frameDocumentNodeId > 0
|
|
284
|
+
? frameDocumentNodeId
|
|
285
|
+
: null,
|
|
286
|
+
iframe_selector: String(rootState?.iframe?.selector || "").slice(0, 300) || null
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function latestInfiniteListReadError(listState = null) {
|
|
291
|
+
const ledger = Array.isArray(listState?.ledger) ? listState.ledger : [];
|
|
292
|
+
for (let index = ledger.length - 1; index >= 0; index -= 1) {
|
|
293
|
+
const item = ledger[index];
|
|
294
|
+
if (item?.event !== "candidate_read_error") continue;
|
|
295
|
+
return {
|
|
296
|
+
at: item.at || null,
|
|
297
|
+
node_id: Number.isInteger(item.node_id) ? item.node_id : null,
|
|
298
|
+
visible_index: Number.isInteger(item.visible_index) ? item.visible_index : null,
|
|
299
|
+
error: String(item.error || "").slice(0, 500) || null
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createRecommendDomStaleForensicEvent(error, {
|
|
306
|
+
eventId = "",
|
|
307
|
+
phase = "",
|
|
308
|
+
operation = "",
|
|
309
|
+
detailStep = "",
|
|
310
|
+
candidateIndex = null,
|
|
311
|
+
candidateKey = "",
|
|
312
|
+
cardNodeId = null,
|
|
313
|
+
rootState = null,
|
|
314
|
+
connectionEpoch = null,
|
|
315
|
+
listState = null,
|
|
316
|
+
counters = null,
|
|
317
|
+
timeline = []
|
|
318
|
+
} = {}) {
|
|
319
|
+
const at = new Date().toISOString();
|
|
320
|
+
const listReadError = latestInfiniteListReadError(listState);
|
|
321
|
+
return {
|
|
322
|
+
schema_version: 1,
|
|
323
|
+
event_id: String(eventId || `recommend_dom_stale_${Date.now()}`).slice(0, 160),
|
|
324
|
+
event_type: "dom_stale",
|
|
325
|
+
at,
|
|
326
|
+
phase: String(phase || "").slice(0, 200) || null,
|
|
327
|
+
operation: String(operation || "").slice(0, 200) || null,
|
|
328
|
+
detail_step: String(detailStep || "").slice(0, 200) || null,
|
|
329
|
+
candidate: {
|
|
330
|
+
index: Number.isInteger(candidateIndex) && candidateIndex >= 0 ? candidateIndex : null,
|
|
331
|
+
key: String(candidateKey || "").slice(0, 300) || null,
|
|
332
|
+
card_node_id: Number.isInteger(cardNodeId) && cardNodeId > 0 ? cardNodeId : null,
|
|
333
|
+
visible_index: listReadError?.visible_index ?? null,
|
|
334
|
+
failing_list_node_id: listReadError?.node_id ?? null
|
|
335
|
+
},
|
|
336
|
+
error: compactCdpFailureDiagnostic(error, {
|
|
337
|
+
fallbackCode: "RECOMMEND_DOM_STALE",
|
|
338
|
+
fallbackPhase: phase
|
|
339
|
+
}),
|
|
340
|
+
pre_recovery_roots: compactRecommendDomRootIdentity(rootState, connectionEpoch),
|
|
341
|
+
candidate_list: compactInfiniteListState(listState || {}),
|
|
342
|
+
counters: counters && typeof counters === "object" ? counters : null,
|
|
343
|
+
lifecycle_timeline: Array.isArray(timeline) ? timeline.slice(-20) : []
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function compactListReadStaleDiagnostic(error, {
|
|
348
|
+
attempt = 0,
|
|
349
|
+
exhausted = false
|
|
350
|
+
} = {}) {
|
|
351
|
+
const cdp = compactCdpFailureDiagnostic(error, {
|
|
352
|
+
fallbackCode: "RECOMMEND_LIST_READ_STALE_NODE",
|
|
353
|
+
fallbackPhase: "recommend:list-read"
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
...cdp,
|
|
357
|
+
code: error?.code || cdp.code || "RECOMMEND_LIST_READ_STALE_NODE",
|
|
358
|
+
message: String(error?.message || cdp.message || error || "Stale recommend list node").slice(0, 500),
|
|
359
|
+
phase: "recommend:list-read",
|
|
360
|
+
attempt,
|
|
361
|
+
exhausted: Boolean(exhausted),
|
|
362
|
+
at: new Date().toISOString()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function annotateListReadStaleFailure(error, diagnostics, {
|
|
367
|
+
exhausted = false,
|
|
368
|
+
recoveryFailed = false
|
|
369
|
+
} = {}) {
|
|
370
|
+
if (!error || typeof error !== "object") return error;
|
|
371
|
+
error.phase = error.phase || "recommend:list-read";
|
|
372
|
+
error.list_read_stale_recovery_attempts = diagnostics;
|
|
373
|
+
if (exhausted) error.list_read_stale_recovery_exhausted = true;
|
|
374
|
+
if (recoveryFailed) error.list_read_stale_recovery_failed = true;
|
|
375
|
+
return error;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function acquireRecommendListReadWithStaleRecovery({
|
|
379
|
+
acquire,
|
|
380
|
+
recover,
|
|
381
|
+
maxRetries = 2,
|
|
382
|
+
onStale = null,
|
|
383
|
+
onRecoveryApplied = null,
|
|
384
|
+
onRecovered = null,
|
|
385
|
+
onExhausted = null
|
|
386
|
+
} = {}) {
|
|
387
|
+
if (typeof acquire !== "function") {
|
|
388
|
+
throw new Error("acquireRecommendListReadWithStaleRecovery requires acquire");
|
|
389
|
+
}
|
|
390
|
+
if (typeof recover !== "function") {
|
|
391
|
+
throw new Error("acquireRecommendListReadWithStaleRecovery requires recover");
|
|
392
|
+
}
|
|
393
|
+
const retryLimit = Math.max(0, Number.isInteger(maxRetries) ? maxRetries : 2);
|
|
394
|
+
const diagnostics = [];
|
|
395
|
+
let acquireAttempt = 0;
|
|
396
|
+
let pendingRecoveryDiagnostic = null;
|
|
397
|
+
while (true) {
|
|
398
|
+
acquireAttempt += 1;
|
|
399
|
+
try {
|
|
400
|
+
const result = await acquire({
|
|
401
|
+
acquireAttempt,
|
|
402
|
+
recoveryCount: diagnostics.filter((item) => item.recovered === true).length
|
|
403
|
+
});
|
|
404
|
+
if (pendingRecoveryDiagnostic) {
|
|
405
|
+
pendingRecoveryDiagnostic.recovered = true;
|
|
406
|
+
pendingRecoveryDiagnostic.recovered_at = new Date().toISOString();
|
|
407
|
+
if (typeof onRecovered === "function") {
|
|
408
|
+
await onRecovered({
|
|
409
|
+
diagnostic: pendingRecoveryDiagnostic,
|
|
410
|
+
diagnostics: diagnostics.slice()
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
pendingRecoveryDiagnostic = null;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
result,
|
|
417
|
+
acquire_attempts: acquireAttempt,
|
|
418
|
+
stale_diagnostics: diagnostics
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (!isStaleRecommendNodeError(error)) throw error;
|
|
422
|
+
pendingRecoveryDiagnostic = null;
|
|
423
|
+
const staleAttempt = diagnostics.length + 1;
|
|
424
|
+
const exhausted = staleAttempt > retryLimit;
|
|
425
|
+
const diagnostic = compactListReadStaleDiagnostic(error, {
|
|
426
|
+
attempt: staleAttempt,
|
|
427
|
+
exhausted
|
|
428
|
+
});
|
|
429
|
+
diagnostics.push(diagnostic);
|
|
430
|
+
if (exhausted) {
|
|
431
|
+
if (typeof onExhausted === "function") {
|
|
432
|
+
await onExhausted({ error, diagnostic, diagnostics: diagnostics.slice() });
|
|
433
|
+
}
|
|
434
|
+
throw annotateListReadStaleFailure(error, diagnostics, { exhausted: true });
|
|
435
|
+
}
|
|
436
|
+
if (typeof onStale === "function") {
|
|
437
|
+
await onStale({ error, diagnostic, diagnostics: diagnostics.slice() });
|
|
438
|
+
}
|
|
439
|
+
let recoveryResult = null;
|
|
440
|
+
try {
|
|
441
|
+
recoveryResult = await recover({ error, diagnostic, diagnostics: diagnostics.slice() });
|
|
442
|
+
} catch (recoveryError) {
|
|
443
|
+
if (recoveryError && typeof recoveryError === "object" && !recoveryError.cause) {
|
|
444
|
+
recoveryError.cause = error;
|
|
445
|
+
}
|
|
446
|
+
throw annotateListReadStaleFailure(recoveryError, diagnostics, {
|
|
447
|
+
recoveryFailed: true
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
diagnostic.recovery_applied = true;
|
|
451
|
+
diagnostic.recovery_mode = recoveryResult?.recovery_mode || "unknown";
|
|
452
|
+
diagnostic.recovery_applied_at = new Date().toISOString();
|
|
453
|
+
pendingRecoveryDiagnostic = diagnostic;
|
|
454
|
+
if (typeof onRecoveryApplied === "function") {
|
|
455
|
+
await onRecoveryApplied({
|
|
456
|
+
error,
|
|
457
|
+
diagnostic,
|
|
458
|
+
recoveryResult,
|
|
459
|
+
diagnostics: diagnostics.slice()
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function recoverRecommendListReadStaleContext({
|
|
467
|
+
staleAttempt = 1,
|
|
468
|
+
listState,
|
|
469
|
+
rootReacquire,
|
|
470
|
+
contextReapply
|
|
471
|
+
} = {}) {
|
|
472
|
+
if (typeof rootReacquire !== "function") {
|
|
473
|
+
throw new Error("recoverRecommendListReadStaleContext requires rootReacquire");
|
|
474
|
+
}
|
|
475
|
+
if (typeof contextReapply !== "function") {
|
|
476
|
+
throw new Error("recoverRecommendListReadStaleContext requires contextReapply");
|
|
477
|
+
}
|
|
478
|
+
const processedKeys = new Set(listState?.processed_keys || []);
|
|
479
|
+
try {
|
|
480
|
+
if (Number(staleAttempt) <= 1) {
|
|
481
|
+
try {
|
|
482
|
+
const rootResult = await rootReacquire();
|
|
483
|
+
if (Number(rootResult?.card_count) <= 0) {
|
|
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
|
+
};
|
|
519
|
+
} finally {
|
|
520
|
+
for (const key of processedKeys) listState?.processed_keys?.add(key);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
93
524
|
function normalizeLabels(labels = []) {
|
|
94
525
|
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
95
526
|
}
|
|
@@ -106,46 +537,46 @@ function isRefreshableListStall(reason = "") {
|
|
|
106
537
|
function normalizeFilter(filter = {}) {
|
|
107
538
|
const filterGroups = Array.isArray(filter.filterGroups)
|
|
108
539
|
? filter.filterGroups
|
|
109
|
-
: Array.isArray(filter.groups)
|
|
110
|
-
? filter.groups
|
|
111
|
-
: [];
|
|
112
|
-
return {
|
|
113
|
-
enabled: filter.enabled !== false,
|
|
114
|
-
currentCityOnly: filter.currentCityOnly === true || filter.current_city_only === true,
|
|
115
|
-
group: String(filter.group || ""),
|
|
116
|
-
labels: normalizeLabels(filter.labels || filter.filterLabels || []),
|
|
117
|
-
selectAllLabels: Boolean(filter.selectAllLabels),
|
|
118
|
-
allowUnlimited: filter.allowUnlimited === true,
|
|
119
|
-
verifySticky: filter.verifySticky === true,
|
|
120
|
-
filterGroups: filterGroups.map((group) => ({
|
|
121
|
-
group: String(group?.group || ""),
|
|
122
|
-
labels: normalizeLabels(group?.labels || group?.filterLabels || []),
|
|
123
|
-
selectAllLabels: group?.selectAllLabels !== false,
|
|
124
|
-
allowUnlimited: group?.allowUnlimited === true,
|
|
125
|
-
verifySticky: group?.verifySticky === true
|
|
126
|
-
})).filter((group) => group.group || group.labels.length)
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function compactFilterResult(filterResult) {
|
|
131
|
-
if (!filterResult) return null;
|
|
132
|
-
return {
|
|
133
|
-
opened_panel: Boolean(filterResult.opened_panel),
|
|
134
|
-
requested_groups: (filterResult.requested_groups || []).map((group) => ({
|
|
135
|
-
group: group.group,
|
|
136
|
-
labels: group.labels || [],
|
|
137
|
-
select_all_labels: group.select_all_labels !== false,
|
|
138
|
-
allow_unlimited: Boolean(group.allow_unlimited),
|
|
139
|
-
verify_sticky: Boolean(group.verify_sticky)
|
|
140
|
-
})),
|
|
141
|
-
effective_groups: (filterResult.sticky_verification?.groups || []).map((group) => ({
|
|
142
|
-
group: group.group,
|
|
143
|
-
requested_labels: group.requested_labels || [],
|
|
144
|
-
active_labels: group.active_labels || [],
|
|
145
|
-
verified: Boolean(group.verified),
|
|
146
|
-
unavailable: Boolean(group.unavailable),
|
|
147
|
-
reason: group.reason || null
|
|
148
|
-
})),
|
|
540
|
+
: Array.isArray(filter.groups)
|
|
541
|
+
? filter.groups
|
|
542
|
+
: [];
|
|
543
|
+
return {
|
|
544
|
+
enabled: filter.enabled !== false,
|
|
545
|
+
currentCityOnly: filter.currentCityOnly === true || filter.current_city_only === true,
|
|
546
|
+
group: String(filter.group || ""),
|
|
547
|
+
labels: normalizeLabels(filter.labels || filter.filterLabels || []),
|
|
548
|
+
selectAllLabels: Boolean(filter.selectAllLabels),
|
|
549
|
+
allowUnlimited: filter.allowUnlimited === true,
|
|
550
|
+
verifySticky: filter.verifySticky === true,
|
|
551
|
+
filterGroups: filterGroups.map((group) => ({
|
|
552
|
+
group: String(group?.group || ""),
|
|
553
|
+
labels: normalizeLabels(group?.labels || group?.filterLabels || []),
|
|
554
|
+
selectAllLabels: group?.selectAllLabels !== false,
|
|
555
|
+
allowUnlimited: group?.allowUnlimited === true,
|
|
556
|
+
verifySticky: group?.verifySticky === true
|
|
557
|
+
})).filter((group) => group.group || group.labels.length)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function compactFilterResult(filterResult) {
|
|
562
|
+
if (!filterResult) return null;
|
|
563
|
+
return {
|
|
564
|
+
opened_panel: Boolean(filterResult.opened_panel),
|
|
565
|
+
requested_groups: (filterResult.requested_groups || []).map((group) => ({
|
|
566
|
+
group: group.group,
|
|
567
|
+
labels: group.labels || [],
|
|
568
|
+
select_all_labels: group.select_all_labels !== false,
|
|
569
|
+
allow_unlimited: Boolean(group.allow_unlimited),
|
|
570
|
+
verify_sticky: Boolean(group.verify_sticky)
|
|
571
|
+
})),
|
|
572
|
+
effective_groups: (filterResult.sticky_verification?.groups || []).map((group) => ({
|
|
573
|
+
group: group.group,
|
|
574
|
+
requested_labels: group.requested_labels || [],
|
|
575
|
+
active_labels: group.active_labels || [],
|
|
576
|
+
verified: Boolean(group.verified),
|
|
577
|
+
unavailable: Boolean(group.unavailable),
|
|
578
|
+
reason: group.reason || null
|
|
579
|
+
})),
|
|
149
580
|
selected_option: filterResult.selected_option
|
|
150
581
|
? {
|
|
151
582
|
group: filterResult.selected_option.group,
|
|
@@ -155,71 +586,71 @@ export function compactFilterResult(filterResult) {
|
|
|
155
586
|
}
|
|
156
587
|
: null,
|
|
157
588
|
selected_options: (filterResult.selected_options || []).map((option) => ({
|
|
158
|
-
group: option.group,
|
|
159
|
-
label: option.label,
|
|
160
|
-
was_active: Boolean(option.was_active),
|
|
161
|
-
clicked: option.clicked !== false
|
|
162
|
-
})),
|
|
163
|
-
unavailable: Boolean(filterResult.unavailable),
|
|
164
|
-
unavailable_groups: filterResult.unavailable_groups || [],
|
|
165
|
-
confirmed: Boolean(filterResult.confirmed),
|
|
166
|
-
sticky_verification: filterResult.sticky_verification || null,
|
|
167
|
-
attempts: {
|
|
168
|
-
initial_close: filterResult.initial_close_attempts || [],
|
|
169
|
-
open: (filterResult.open_attempts || []).map((attempt) => ({
|
|
170
|
-
selector: attempt.selector || null,
|
|
171
|
-
node_id: attempt.node_id || null,
|
|
172
|
-
click_target: attempt.click_target || null
|
|
173
|
-
})),
|
|
174
|
-
confirmation: (filterResult.confirm_attempts || []).map((attempt) => ({
|
|
175
|
-
node_id: attempt.node_id || null,
|
|
176
|
-
label: attempt.label || null,
|
|
177
|
-
clicked: Boolean(attempt.clicked),
|
|
178
|
-
errors: (attempt.errors || []).map((error) => ({
|
|
179
|
-
node_id: error.node_id || null,
|
|
180
|
-
message: error.message || String(error)
|
|
181
|
-
}))
|
|
182
|
-
}))
|
|
183
|
-
},
|
|
184
|
-
before_counts: filterResult.before_counts,
|
|
185
|
-
after_confirm_counts: filterResult.after_confirm_counts
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function compactCurrentCityOnlyResult(result) {
|
|
190
|
-
if (!result) return null;
|
|
191
|
-
return {
|
|
192
|
-
requested: Boolean(result.requested),
|
|
193
|
-
effective: typeof result.effective === "boolean" ? result.effective : null,
|
|
194
|
-
available: result.available !== false,
|
|
195
|
-
unavailable: Boolean(result.unavailable),
|
|
196
|
-
reason: result.reason || null,
|
|
197
|
-
clicked: Boolean(result.clicked),
|
|
198
|
-
current_city_label: result.current_city_label || null,
|
|
199
|
-
before: result.before || null,
|
|
200
|
-
after_toggle: result.after_toggle || null,
|
|
201
|
-
confirmation: result.confirmation || null,
|
|
202
|
-
sticky_verification: result.sticky_verification
|
|
203
|
-
? {
|
|
204
|
-
verified: Boolean(result.sticky_verification.verified),
|
|
205
|
-
expected: Boolean(result.sticky_verification.expected),
|
|
206
|
-
actual: typeof result.sticky_verification.actual === "boolean"
|
|
207
|
-
? result.sticky_verification.actual
|
|
208
|
-
: null,
|
|
209
|
-
state_source: result.sticky_verification.state_source || null,
|
|
210
|
-
close_confirmation: result.sticky_verification.close_confirmation || null
|
|
211
|
-
}
|
|
212
|
-
: null,
|
|
213
|
-
attempts: Array.isArray(result.attempts) ? result.attempts : [],
|
|
214
|
-
evidence: result.evidence || null
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function compactJobSelection(jobSelection) {
|
|
219
|
-
if (!jobSelection) return null;
|
|
220
|
-
return {
|
|
221
|
-
requested: jobSelection.requested || "",
|
|
222
|
-
selected: Boolean(jobSelection.selected),
|
|
589
|
+
group: option.group,
|
|
590
|
+
label: option.label,
|
|
591
|
+
was_active: Boolean(option.was_active),
|
|
592
|
+
clicked: option.clicked !== false
|
|
593
|
+
})),
|
|
594
|
+
unavailable: Boolean(filterResult.unavailable),
|
|
595
|
+
unavailable_groups: filterResult.unavailable_groups || [],
|
|
596
|
+
confirmed: Boolean(filterResult.confirmed),
|
|
597
|
+
sticky_verification: filterResult.sticky_verification || null,
|
|
598
|
+
attempts: {
|
|
599
|
+
initial_close: filterResult.initial_close_attempts || [],
|
|
600
|
+
open: (filterResult.open_attempts || []).map((attempt) => ({
|
|
601
|
+
selector: attempt.selector || null,
|
|
602
|
+
node_id: attempt.node_id || null,
|
|
603
|
+
click_target: attempt.click_target || null
|
|
604
|
+
})),
|
|
605
|
+
confirmation: (filterResult.confirm_attempts || []).map((attempt) => ({
|
|
606
|
+
node_id: attempt.node_id || null,
|
|
607
|
+
label: attempt.label || null,
|
|
608
|
+
clicked: Boolean(attempt.clicked),
|
|
609
|
+
errors: (attempt.errors || []).map((error) => ({
|
|
610
|
+
node_id: error.node_id || null,
|
|
611
|
+
message: error.message || String(error)
|
|
612
|
+
}))
|
|
613
|
+
}))
|
|
614
|
+
},
|
|
615
|
+
before_counts: filterResult.before_counts,
|
|
616
|
+
after_confirm_counts: filterResult.after_confirm_counts
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function compactCurrentCityOnlyResult(result) {
|
|
621
|
+
if (!result) return null;
|
|
622
|
+
return {
|
|
623
|
+
requested: Boolean(result.requested),
|
|
624
|
+
effective: typeof result.effective === "boolean" ? result.effective : null,
|
|
625
|
+
available: result.available !== false,
|
|
626
|
+
unavailable: Boolean(result.unavailable),
|
|
627
|
+
reason: result.reason || null,
|
|
628
|
+
clicked: Boolean(result.clicked),
|
|
629
|
+
current_city_label: result.current_city_label || null,
|
|
630
|
+
before: result.before || null,
|
|
631
|
+
after_toggle: result.after_toggle || null,
|
|
632
|
+
confirmation: result.confirmation || null,
|
|
633
|
+
sticky_verification: result.sticky_verification
|
|
634
|
+
? {
|
|
635
|
+
verified: Boolean(result.sticky_verification.verified),
|
|
636
|
+
expected: Boolean(result.sticky_verification.expected),
|
|
637
|
+
actual: typeof result.sticky_verification.actual === "boolean"
|
|
638
|
+
? result.sticky_verification.actual
|
|
639
|
+
: null,
|
|
640
|
+
state_source: result.sticky_verification.state_source || null,
|
|
641
|
+
close_confirmation: result.sticky_verification.close_confirmation || null
|
|
642
|
+
}
|
|
643
|
+
: null,
|
|
644
|
+
attempts: Array.isArray(result.attempts) ? result.attempts : [],
|
|
645
|
+
evidence: result.evidence || null
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function compactJobSelection(jobSelection) {
|
|
650
|
+
if (!jobSelection) return null;
|
|
651
|
+
return {
|
|
652
|
+
requested: jobSelection.requested || "",
|
|
653
|
+
selected: Boolean(jobSelection.selected),
|
|
223
654
|
already_current: Boolean(jobSelection.already_current),
|
|
224
655
|
reason: jobSelection.reason || null,
|
|
225
656
|
selected_option: jobSelection.selected_option || null,
|
|
@@ -454,6 +885,7 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
454
885
|
method: refreshAttempt.method || "",
|
|
455
886
|
reason: refreshAttempt.reason || null,
|
|
456
887
|
error: refreshAttempt.error || null,
|
|
888
|
+
error_diagnostic: refreshAttempt.error_diagnostic || null,
|
|
457
889
|
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
458
890
|
target_url: refreshAttempt.target_url || null,
|
|
459
891
|
card_count: refreshAttempt.card_count || 0,
|
|
@@ -470,36 +902,40 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
470
902
|
ok: Boolean(attempt.ok),
|
|
471
903
|
method: attempt.method || "",
|
|
472
904
|
reason: attempt.reason || null,
|
|
473
|
-
error: attempt.error || null,
|
|
474
|
-
label: attempt.label || null,
|
|
475
|
-
before_card_count: attempt.before_card_count || 0,
|
|
476
|
-
after_card_count: attempt.after_card_count || 0,
|
|
477
|
-
card_count: attempt.card_count || 0,
|
|
478
|
-
elapsed_ms: attempt.elapsed_ms || 0,
|
|
479
|
-
current_city_only: compactCurrentCityOnlyResult(attempt.current_city_only),
|
|
480
|
-
current_city_only_attempts: (attempt.current_city_only_attempts || []).map((cityAttempt) => ({
|
|
481
|
-
ok: Boolean(cityAttempt.ok),
|
|
482
|
-
method: cityAttempt.method || "current_city_only_reapply",
|
|
483
|
-
reason: cityAttempt.reason || null,
|
|
484
|
-
error: cityAttempt.error || null,
|
|
485
|
-
attempt: cityAttempt.attempt || 0,
|
|
486
|
-
result: compactCurrentCityOnlyResult(cityAttempt.result)
|
|
487
|
-
})),
|
|
488
|
-
filter: compactFilterResult(attempt.filter)
|
|
489
|
-
})),
|
|
490
|
-
current_city_only_attempts: (refreshAttempt.current_city_only_attempts || []).map((attempt) => ({
|
|
491
|
-
ok: Boolean(attempt.ok),
|
|
492
|
-
method: attempt.method || "current_city_only_reapply",
|
|
493
|
-
reason: attempt.reason || null,
|
|
494
|
-
error: attempt.error || null,
|
|
495
|
-
attempt: attempt.attempt || 0,
|
|
496
|
-
result: compactCurrentCityOnlyResult(attempt.result)
|
|
497
|
-
})),
|
|
498
|
-
filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
|
|
499
|
-
ok: Boolean(attempt.ok),
|
|
500
|
-
method: attempt.method || "filter_reapply",
|
|
501
|
-
reason: attempt.reason || null,
|
|
502
905
|
error: attempt.error || null,
|
|
906
|
+
error_diagnostic: attempt.error_diagnostic || null,
|
|
907
|
+
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 || 0,
|
|
911
|
+
elapsed_ms: attempt.elapsed_ms || 0,
|
|
912
|
+
current_city_only: compactCurrentCityOnlyResult(attempt.current_city_only),
|
|
913
|
+
current_city_only_attempts: (attempt.current_city_only_attempts || []).map((cityAttempt) => ({
|
|
914
|
+
ok: Boolean(cityAttempt.ok),
|
|
915
|
+
method: cityAttempt.method || "current_city_only_reapply",
|
|
916
|
+
reason: cityAttempt.reason || null,
|
|
917
|
+
error: cityAttempt.error || null,
|
|
918
|
+
error_diagnostic: cityAttempt.error_diagnostic || null,
|
|
919
|
+
attempt: cityAttempt.attempt || 0,
|
|
920
|
+
result: compactCurrentCityOnlyResult(cityAttempt.result)
|
|
921
|
+
})),
|
|
922
|
+
filter: compactFilterResult(attempt.filter)
|
|
923
|
+
})),
|
|
924
|
+
current_city_only_attempts: (refreshAttempt.current_city_only_attempts || []).map((attempt) => ({
|
|
925
|
+
ok: Boolean(attempt.ok),
|
|
926
|
+
method: attempt.method || "current_city_only_reapply",
|
|
927
|
+
reason: attempt.reason || null,
|
|
928
|
+
error: attempt.error || null,
|
|
929
|
+
error_diagnostic: attempt.error_diagnostic || null,
|
|
930
|
+
attempt: attempt.attempt || 0,
|
|
931
|
+
result: compactCurrentCityOnlyResult(attempt.result)
|
|
932
|
+
})),
|
|
933
|
+
filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
|
|
934
|
+
ok: Boolean(attempt.ok),
|
|
935
|
+
method: attempt.method || "filter_reapply",
|
|
936
|
+
reason: attempt.reason || null,
|
|
937
|
+
error: attempt.error || null,
|
|
938
|
+
error_diagnostic: attempt.error_diagnostic || null,
|
|
503
939
|
attempt: attempt.attempt || 0
|
|
504
940
|
})),
|
|
505
941
|
job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
|
|
@@ -507,19 +943,20 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
507
943
|
method: attempt.method || "job_select",
|
|
508
944
|
reason: attempt.reason || null,
|
|
509
945
|
error: attempt.error || null,
|
|
946
|
+
error_diagnostic: attempt.error_diagnostic || null,
|
|
510
947
|
attempt: attempt.attempt || 0,
|
|
511
948
|
iframe_document_node_id: attempt.iframe_document_node_id || 0,
|
|
512
|
-
selected: Boolean(attempt.selected),
|
|
513
|
-
selection_reason: attempt.selection_reason || null
|
|
514
|
-
})),
|
|
515
|
-
job_selection: compactJobSelection(refreshAttempt.job_selection),
|
|
516
|
-
page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
|
|
517
|
-
current_city_only: compactCurrentCityOnlyResult(refreshAttempt.current_city_only),
|
|
518
|
-
filter: compactFilterResult(refreshAttempt.filter)
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export function countRecommendResultStatuses(results = [], {
|
|
949
|
+
selected: Boolean(attempt.selected),
|
|
950
|
+
selection_reason: attempt.selection_reason || null
|
|
951
|
+
})),
|
|
952
|
+
job_selection: compactJobSelection(refreshAttempt.job_selection),
|
|
953
|
+
page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
|
|
954
|
+
current_city_only: compactCurrentCityOnlyResult(refreshAttempt.current_city_only),
|
|
955
|
+
filter: compactFilterResult(refreshAttempt.filter)
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function countRecommendResultStatuses(results = [], {
|
|
523
960
|
greetCount = 0
|
|
524
961
|
} = {}) {
|
|
525
962
|
return {
|
|
@@ -573,12 +1010,16 @@ function compactCloseResult(closeResult) {
|
|
|
573
1010
|
return result;
|
|
574
1011
|
}
|
|
575
1012
|
|
|
576
|
-
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
577
|
-
if (!error) return null;
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
};
|
|
1013
|
+
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
1014
|
+
if (!error) return null;
|
|
1015
|
+
const cdpDiagnostic = compactCdpFailureDiagnostic(error, {
|
|
1016
|
+
fallbackCode,
|
|
1017
|
+
fallbackPhase: error?.phase || ""
|
|
1018
|
+
});
|
|
1019
|
+
const result = {
|
|
1020
|
+
code: error.code || fallbackCode,
|
|
1021
|
+
message: error.message || String(error)
|
|
1022
|
+
};
|
|
582
1023
|
if (error.close_result) {
|
|
583
1024
|
result.close_result = compactCloseResult(error.close_result);
|
|
584
1025
|
}
|
|
@@ -588,6 +1029,15 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
|
588
1029
|
if (error.refresh_attempt) {
|
|
589
1030
|
result.refresh_attempt = error.refresh_attempt;
|
|
590
1031
|
}
|
|
1032
|
+
if (error.error_diagnostic) {
|
|
1033
|
+
result.error_diagnostic = error.error_diagnostic;
|
|
1034
|
+
} else if (
|
|
1035
|
+
cdpDiagnostic.cdp_method
|
|
1036
|
+
|| cdpDiagnostic.cdp_connection_epoch !== null
|
|
1037
|
+
|| cdpDiagnostic.cdp_node_id !== null
|
|
1038
|
+
) {
|
|
1039
|
+
result.error_diagnostic = cdpDiagnostic;
|
|
1040
|
+
}
|
|
591
1041
|
if (error.list_end_reason) {
|
|
592
1042
|
result.list_end_reason = error.list_end_reason;
|
|
593
1043
|
}
|
|
@@ -628,7 +1078,45 @@ function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "")
|
|
|
628
1078
|
return error;
|
|
629
1079
|
}
|
|
630
1080
|
|
|
631
|
-
function
|
|
1081
|
+
function findRecommendRefreshErrorDiagnostic(refreshAttempt) {
|
|
1082
|
+
if (!refreshAttempt || typeof refreshAttempt !== "object") return null;
|
|
1083
|
+
if (refreshAttempt.error_diagnostic) return refreshAttempt.error_diagnostic;
|
|
1084
|
+
const candidates = [
|
|
1085
|
+
...(refreshAttempt.attempts || []),
|
|
1086
|
+
...(refreshAttempt.current_city_only_attempts || []),
|
|
1087
|
+
...(refreshAttempt.filter_reapply_attempts || []),
|
|
1088
|
+
...(refreshAttempt.job_selection_attempts || [])
|
|
1089
|
+
].reverse();
|
|
1090
|
+
for (const attempt of candidates) {
|
|
1091
|
+
if (attempt?.error_diagnostic) return attempt.error_diagnostic;
|
|
1092
|
+
const nestedCityAttempts = (attempt?.current_city_only_attempts || []).slice().reverse();
|
|
1093
|
+
for (const cityAttempt of nestedCityAttempts) {
|
|
1094
|
+
if (cityAttempt?.error_diagnostic) return cityAttempt.error_diagnostic;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function attachRecommendRefreshErrorDiagnostic(error, refreshAttempt) {
|
|
1101
|
+
const diagnostic = findRecommendRefreshErrorDiagnostic(refreshAttempt);
|
|
1102
|
+
if (!error || !diagnostic) return error;
|
|
1103
|
+
error.error_diagnostic = diagnostic;
|
|
1104
|
+
for (const key of [
|
|
1105
|
+
"cdp_method",
|
|
1106
|
+
"cdp_at",
|
|
1107
|
+
"cdp_node_id",
|
|
1108
|
+
"cdp_backend_node_id",
|
|
1109
|
+
"cdp_search_id",
|
|
1110
|
+
"cdp_param_keys"
|
|
1111
|
+
]) {
|
|
1112
|
+
if (diagnostic[key] !== undefined && error[key] === undefined) {
|
|
1113
|
+
error[key] = diagnostic[key];
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return error;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export function createRecommendRefreshFailureError(refreshAttempt, {
|
|
632
1120
|
listEndReason = "",
|
|
633
1121
|
targetCount = 0,
|
|
634
1122
|
passedCount = 0
|
|
@@ -641,15 +1129,21 @@ function createRecommendRefreshFailureError(refreshAttempt, {
|
|
|
641
1129
|
error.list_end_reason = listEndReason || null;
|
|
642
1130
|
error.target_count = targetCount;
|
|
643
1131
|
error.passed_count = passedCount;
|
|
644
|
-
return error;
|
|
1132
|
+
return attachRecommendRefreshErrorDiagnostic(error, refreshAttempt);
|
|
645
1133
|
}
|
|
646
1134
|
|
|
647
|
-
export function isRecoverableImageCaptureError(error) {
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1135
|
+
export function isRecoverableImageCaptureError(error) {
|
|
1136
|
+
if (isRecoverableImageCaptureWorkflowError(error)) return true;
|
|
1137
|
+
if (isStaleRecommendNodeError(error)) return true;
|
|
1138
|
+
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
export function shouldFailClosedRecommendImageAcquisition(detailResult = null) {
|
|
1142
|
+
return isFailedClosedImageAcquisition({
|
|
1143
|
+
source: detailResult?.cv_acquisition?.source,
|
|
1144
|
+
imageEvidence: detailResult?.image_evidence
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
653
1147
|
|
|
654
1148
|
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
655
1149
|
const resolved = String(basePath || "").trim();
|
|
@@ -781,9 +1275,19 @@ export async function runRecommendWorkflow({
|
|
|
781
1275
|
humanRestEnabled = false,
|
|
782
1276
|
humanBehavior = null,
|
|
783
1277
|
skipRecentColleagueContacted = true,
|
|
784
|
-
colleagueContactWindowDays = 14
|
|
1278
|
+
colleagueContactWindowDays = 14,
|
|
1279
|
+
debugTestMode = false,
|
|
1280
|
+
debugForceListEndAfterProcessed = null,
|
|
1281
|
+
debugForceContextRecoveryAfterProcessed = null,
|
|
1282
|
+
debugForceCdpReconnectAfterProcessed = null
|
|
785
1283
|
} = {}, runControl) {
|
|
786
1284
|
if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
|
|
1285
|
+
const debugBoundary = createRecommendDebugBoundaryController({
|
|
1286
|
+
debugTestMode,
|
|
1287
|
+
debugForceListEndAfterProcessed,
|
|
1288
|
+
debugForceContextRecoveryAfterProcessed,
|
|
1289
|
+
debugForceCdpReconnectAfterProcessed
|
|
1290
|
+
});
|
|
787
1291
|
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
788
1292
|
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
789
1293
|
});
|
|
@@ -837,25 +1341,164 @@ export async function runRecommendWorkflow({
|
|
|
837
1341
|
const results = [];
|
|
838
1342
|
const refreshAttempts = [];
|
|
839
1343
|
let refreshRounds = 0;
|
|
840
|
-
let contextRecoveryAttempts = 0;
|
|
1344
|
+
let contextRecoveryAttempts = 0;
|
|
841
1345
|
let greetCount = 0;
|
|
842
1346
|
const candidateRecoveryCounts = new Map();
|
|
843
|
-
|
|
844
|
-
let
|
|
845
|
-
let
|
|
846
|
-
let
|
|
847
|
-
let
|
|
848
|
-
let
|
|
849
|
-
let
|
|
850
|
-
let
|
|
851
|
-
|
|
1347
|
+
const imageCaptureWorkflowRetries = createImageCaptureWorkflowRetryTracker();
|
|
1348
|
+
let jobSelection = null;
|
|
1349
|
+
let pageScopeSelection = null;
|
|
1350
|
+
let currentCityOnlyResult = null;
|
|
1351
|
+
let filterResult = null;
|
|
1352
|
+
let rootState = null;
|
|
1353
|
+
let cardNodeIds = [];
|
|
1354
|
+
let listEndReason = "";
|
|
1355
|
+
let lastHumanEvent = null;
|
|
1356
|
+
let debugForceReconnectPending = null;
|
|
1357
|
+
let listReadStaleRecoveryAttempts = 0;
|
|
1358
|
+
let listReadStaleRecoveryApplied = 0;
|
|
1359
|
+
let listReadStaleRecoveries = 0;
|
|
1360
|
+
let lastListReadStaleDiagnostic = null;
|
|
1361
|
+
let lastListReadRecoveryMode = null;
|
|
1362
|
+
const listReadStaleDiagnostics = [];
|
|
1363
|
+
let domStaleEventCount = 0;
|
|
1364
|
+
let currentDomOperation = "recommend:initialize";
|
|
1365
|
+
const domLifecycleTimeline = [];
|
|
1366
|
+
const domStaleForensics = [];
|
|
1367
|
+
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
852
1368
|
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
853
1369
|
containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
|
|
854
1370
|
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
855
1371
|
itemSelectors: [RECOMMEND_CARD_SELECTOR],
|
|
856
1372
|
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
857
|
-
validateViewportPoint: true
|
|
858
|
-
}));
|
|
1373
|
+
validateViewportPoint: true
|
|
1374
|
+
}));
|
|
1375
|
+
|
|
1376
|
+
function currentConnectionEpoch() {
|
|
1377
|
+
const epoch = Number(client?.__connectionEpoch);
|
|
1378
|
+
return Number.isInteger(epoch) && epoch > 0 ? epoch : null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function compactLifecycleUrl(value = "") {
|
|
1382
|
+
const text = String(value || "").trim();
|
|
1383
|
+
if (!text) return null;
|
|
1384
|
+
try {
|
|
1385
|
+
const parsed = new URL(text);
|
|
1386
|
+
return `${parsed.origin}${parsed.pathname}`.slice(0, 500);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return text.split(/[?#]/, 1)[0].slice(0, 500) || null;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function recordDomLifecycleEvent(type, payload = {}) {
|
|
1393
|
+
const frame = payload?.frame || payload || {};
|
|
1394
|
+
const event = {
|
|
1395
|
+
at: new Date().toISOString(),
|
|
1396
|
+
type: String(type || "unknown").slice(0, 100),
|
|
1397
|
+
operation: currentDomOperation,
|
|
1398
|
+
connection_epoch: currentConnectionEpoch(),
|
|
1399
|
+
frame_id: String(frame?.id || payload?.frameId || "").slice(0, 200) || null,
|
|
1400
|
+
parent_frame_id: String(frame?.parentId || "").slice(0, 200) || null,
|
|
1401
|
+
loader_id: String(frame?.loaderId || "").slice(0, 200) || null,
|
|
1402
|
+
url: compactLifecycleUrl(frame?.url || "")
|
|
1403
|
+
};
|
|
1404
|
+
domLifecycleTimeline.push(event);
|
|
1405
|
+
if (domLifecycleTimeline.length > 40) domLifecycleTimeline.splice(0, domLifecycleTimeline.length - 40);
|
|
1406
|
+
return event;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function subscribeDomLifecycleEvents() {
|
|
1410
|
+
const subscriptions = [
|
|
1411
|
+
[client?.DOM, "documentUpdated", "DOM.documentUpdated"],
|
|
1412
|
+
[client?.Page, "frameNavigated", "Page.frameNavigated"],
|
|
1413
|
+
[client?.Page, "frameDetached", "Page.frameDetached"],
|
|
1414
|
+
[client?.Page, "frameStartedLoading", "Page.frameStartedLoading"],
|
|
1415
|
+
[client?.Page, "frameStoppedLoading", "Page.frameStoppedLoading"]
|
|
1416
|
+
];
|
|
1417
|
+
for (const [domain, eventName, label] of subscriptions) {
|
|
1418
|
+
if (typeof domain?.[eventName] !== "function") continue;
|
|
1419
|
+
try {
|
|
1420
|
+
domain[eventName]((payload = {}) => recordDomLifecycleEvent(label, payload));
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Lifecycle observation is diagnostic-only and must not alter the run.
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function checkpointDomStaleForensic(error, {
|
|
1428
|
+
phase = "",
|
|
1429
|
+
operation = currentDomOperation,
|
|
1430
|
+
detailStep = "",
|
|
1431
|
+
candidateIndex = null,
|
|
1432
|
+
candidateKey = "",
|
|
1433
|
+
cardNodeId = null
|
|
1434
|
+
} = {}) {
|
|
1435
|
+
if (!isStaleRecommendNodeError(error)) return null;
|
|
1436
|
+
domStaleEventCount += 1;
|
|
1437
|
+
const event = createRecommendDomStaleForensicEvent(error, {
|
|
1438
|
+
eventId: `recommend_dom_stale_${runControl?.runId || "run"}_${domStaleEventCount}`,
|
|
1439
|
+
phase,
|
|
1440
|
+
operation,
|
|
1441
|
+
detailStep,
|
|
1442
|
+
candidateIndex,
|
|
1443
|
+
candidateKey,
|
|
1444
|
+
cardNodeId,
|
|
1445
|
+
rootState,
|
|
1446
|
+
connectionEpoch: currentConnectionEpoch(),
|
|
1447
|
+
listState,
|
|
1448
|
+
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
1449
|
+
timeline: domLifecycleTimeline
|
|
1450
|
+
});
|
|
1451
|
+
domStaleForensics.push(event);
|
|
1452
|
+
if (domStaleForensics.length > 12) domStaleForensics.splice(0, domStaleForensics.length - 12);
|
|
1453
|
+
runControl.checkpoint({
|
|
1454
|
+
dom_stale_forensic: event,
|
|
1455
|
+
dom_stale_forensics: domStaleForensics.slice(),
|
|
1456
|
+
candidate_list: compactInfiniteListState(listState)
|
|
1457
|
+
});
|
|
1458
|
+
updateRecommendProgress({
|
|
1459
|
+
dom_stale_events: domStaleEventCount,
|
|
1460
|
+
last_dom_stale_phase: phase || null,
|
|
1461
|
+
last_dom_stale_operation: operation || null
|
|
1462
|
+
});
|
|
1463
|
+
return event;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function checkpointDomStaleRecovery(event, {
|
|
1467
|
+
status = "unknown",
|
|
1468
|
+
recoveryResult = null,
|
|
1469
|
+
recoveryError = null
|
|
1470
|
+
} = {}) {
|
|
1471
|
+
if (!event) return null;
|
|
1472
|
+
event.recovery = {
|
|
1473
|
+
status: String(status || "unknown").slice(0, 100),
|
|
1474
|
+
at: new Date().toISOString(),
|
|
1475
|
+
mode: String(recoveryResult?.recovery_mode || recoveryResult?.method || "").slice(0, 100) || null,
|
|
1476
|
+
escalated_from: String(recoveryResult?.escalated_from || "").slice(0, 100) || null,
|
|
1477
|
+
ok: recoveryResult?.ok === undefined ? null : Boolean(recoveryResult.ok),
|
|
1478
|
+
method: String(recoveryResult?.method || recoveryResult?.context_reapply?.method || "").slice(0, 100) || null,
|
|
1479
|
+
card_count: Number.isInteger(recoveryResult?.card_count)
|
|
1480
|
+
? recoveryResult.card_count
|
|
1481
|
+
: Number.isInteger(recoveryResult?.root_reacquire?.card_count)
|
|
1482
|
+
? recoveryResult.root_reacquire.card_count
|
|
1483
|
+
: Number.isInteger(recoveryResult?.context_reapply?.card_count)
|
|
1484
|
+
? recoveryResult.context_reapply.card_count
|
|
1485
|
+
: null,
|
|
1486
|
+
error: recoveryError ? compactCdpFailureDiagnostic(recoveryError, {
|
|
1487
|
+
fallbackCode: "RECOMMEND_DOM_STALE_RECOVERY_FAILED",
|
|
1488
|
+
fallbackPhase: "recommend:recover-context"
|
|
1489
|
+
}) : null
|
|
1490
|
+
};
|
|
1491
|
+
event.post_recovery_roots = compactRecommendDomRootIdentity(rootState, currentConnectionEpoch());
|
|
1492
|
+
event.lifecycle_timeline = domLifecycleTimeline.slice(-20);
|
|
1493
|
+
runControl.checkpoint({
|
|
1494
|
+
dom_stale_forensic: event,
|
|
1495
|
+
dom_stale_forensics: domStaleForensics.slice(),
|
|
1496
|
+
candidate_list: compactInfiniteListState(listState)
|
|
1497
|
+
});
|
|
1498
|
+
return event;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
subscribeDomLifecycleEvents();
|
|
859
1502
|
|
|
860
1503
|
function recordHumanEvent(event = null) {
|
|
861
1504
|
if (!event) return lastHumanEvent;
|
|
@@ -892,6 +1535,7 @@ export async function runRecommendWorkflow({
|
|
|
892
1535
|
target_count: targetPassCount,
|
|
893
1536
|
target_count_semantics: "passed_candidates",
|
|
894
1537
|
...counts,
|
|
1538
|
+
transient_recovered: counts.transient_recovered + listReadStaleRecoveries,
|
|
895
1539
|
screening_mode: normalizedScreeningMode,
|
|
896
1540
|
unique_seen: listSnapshot.seen_count,
|
|
897
1541
|
scroll_count: listSnapshot.scroll_count,
|
|
@@ -902,19 +1546,28 @@ export async function runRecommendWorkflow({
|
|
|
902
1546
|
viewport_checks: viewportGuard.getStats().checks,
|
|
903
1547
|
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
904
1548
|
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
905
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
906
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
907
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
908
|
-
skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
|
|
909
|
-
colleague_contact_window_days: normalizedColleagueContactWindowDays,
|
|
910
|
-
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
911
|
-
current_city_only_effective: currentCityOnlyResult?.effective ?? null,
|
|
912
|
-
current_city_only_unavailable: Boolean(currentCityOnlyResult?.unavailable),
|
|
913
|
-
human_rest_count: humanRestState.rest_count,
|
|
914
|
-
human_rest_ms: humanRestState.total_rest_ms,
|
|
915
|
-
last_human_event: lastHumanEvent,
|
|
916
|
-
|
|
917
|
-
|
|
1549
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1550
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1551
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1552
|
+
skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
|
|
1553
|
+
colleague_contact_window_days: normalizedColleagueContactWindowDays,
|
|
1554
|
+
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
1555
|
+
current_city_only_effective: currentCityOnlyResult?.effective ?? null,
|
|
1556
|
+
current_city_only_unavailable: Boolean(currentCityOnlyResult?.unavailable),
|
|
1557
|
+
human_rest_count: humanRestState.rest_count,
|
|
1558
|
+
human_rest_ms: humanRestState.total_rest_ms,
|
|
1559
|
+
last_human_event: lastHumanEvent,
|
|
1560
|
+
debug_boundary_mode: debugBoundary.config.mode,
|
|
1561
|
+
debug_boundary_threshold: debugBoundary.config.threshold,
|
|
1562
|
+
debug_boundary_triggered: debugBoundary.getState().triggered,
|
|
1563
|
+
debug_boundary_trigger_count: debugBoundary.getState().trigger_count,
|
|
1564
|
+
list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
|
|
1565
|
+
list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
|
|
1566
|
+
list_read_stale_recoveries: listReadStaleRecoveries,
|
|
1567
|
+
last_list_read_recovery_mode: lastListReadRecoveryMode,
|
|
1568
|
+
last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
|
|
1569
|
+
...extra
|
|
1570
|
+
});
|
|
918
1571
|
}
|
|
919
1572
|
|
|
920
1573
|
function checkpointInProgressCandidate({
|
|
@@ -951,27 +1604,27 @@ export async function runRecommendWorkflow({
|
|
|
951
1604
|
async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
|
|
952
1605
|
forceRecentNotView = true
|
|
953
1606
|
} = {}) {
|
|
954
|
-
await runControl.waitIfPaused();
|
|
955
|
-
runControl.throwIfCanceled();
|
|
956
|
-
const started = Date.now();
|
|
957
|
-
runControl.setPhase("recommend:recover-context");
|
|
958
|
-
contextRecoveryAttempts += 1;
|
|
959
|
-
const effectiveForceRecentNotView = normalizedFilter.enabled && forceRecentNotView;
|
|
960
|
-
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
961
|
-
rootState,
|
|
962
|
-
jobLabel,
|
|
963
|
-
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
964
|
-
fallbackPageScope: normalizedFallbackPageScope,
|
|
965
|
-
filter: normalizedFilter,
|
|
966
|
-
preferEndRefreshButton: false,
|
|
967
|
-
forceNavigate: true,
|
|
968
|
-
targetUrl: targetUrl || RECOMMEND_TARGET_URL,
|
|
969
|
-
forceRecentNotView: effectiveForceRecentNotView,
|
|
970
|
-
cardTimeoutMs,
|
|
971
|
-
buttonSettleMs: refreshButtonSettleMs,
|
|
972
|
-
reloadSettleMs: refreshReloadSettleMs
|
|
973
|
-
});
|
|
974
|
-
let blockingPanelClose = null;
|
|
1607
|
+
await runControl.waitIfPaused();
|
|
1608
|
+
runControl.throwIfCanceled();
|
|
1609
|
+
const started = Date.now();
|
|
1610
|
+
runControl.setPhase("recommend:recover-context");
|
|
1611
|
+
contextRecoveryAttempts += 1;
|
|
1612
|
+
const effectiveForceRecentNotView = normalizedFilter.enabled && forceRecentNotView;
|
|
1613
|
+
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
1614
|
+
rootState,
|
|
1615
|
+
jobLabel,
|
|
1616
|
+
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
1617
|
+
fallbackPageScope: normalizedFallbackPageScope,
|
|
1618
|
+
filter: normalizedFilter,
|
|
1619
|
+
preferEndRefreshButton: false,
|
|
1620
|
+
forceNavigate: true,
|
|
1621
|
+
targetUrl: targetUrl || RECOMMEND_TARGET_URL,
|
|
1622
|
+
forceRecentNotView: effectiveForceRecentNotView,
|
|
1623
|
+
cardTimeoutMs,
|
|
1624
|
+
buttonSettleMs: refreshButtonSettleMs,
|
|
1625
|
+
reloadSettleMs: refreshReloadSettleMs
|
|
1626
|
+
});
|
|
1627
|
+
let blockingPanelClose = null;
|
|
975
1628
|
if (refreshResult.ok) {
|
|
976
1629
|
blockingPanelClose = await closeRecommendBlockingPanels(client, {
|
|
977
1630
|
attemptsLimit: 2,
|
|
@@ -995,27 +1648,33 @@ export async function runRecommendWorkflow({
|
|
|
995
1648
|
refresh: compactRefresh,
|
|
996
1649
|
counters: countRecommendResultStatuses(results, { greetCount })
|
|
997
1650
|
},
|
|
998
|
-
candidate_list: compactInfiniteListState(listState)
|
|
999
|
-
});
|
|
1000
|
-
if (!refreshResult.ok) {
|
|
1001
|
-
updateRecommendProgress({
|
|
1002
|
-
refresh_method: refreshResult.method || null,
|
|
1003
|
-
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
1004
|
-
recovery_reason: reason
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1651
|
+
candidate_list: compactInfiniteListState(listState)
|
|
1652
|
+
});
|
|
1653
|
+
if (!refreshResult.ok) {
|
|
1654
|
+
updateRecommendProgress({
|
|
1655
|
+
refresh_method: refreshResult.method || null,
|
|
1656
|
+
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
1657
|
+
recovery_reason: reason
|
|
1658
|
+
});
|
|
1659
|
+
const recoveryError = new Error(
|
|
1660
|
+
`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`
|
|
1661
|
+
);
|
|
1662
|
+
recoveryError.code = "RECOMMEND_CONTEXT_RECOVERY_FAILED";
|
|
1663
|
+
recoveryError.refresh_attempt = compactRefresh;
|
|
1664
|
+
recoveryError.recovery_reason = reason;
|
|
1665
|
+
throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
|
|
1666
|
+
}
|
|
1667
|
+
if (refreshResult.current_city_only) {
|
|
1668
|
+
currentCityOnlyResult = refreshResult.current_city_only;
|
|
1669
|
+
}
|
|
1670
|
+
if (refreshResult.filter) {
|
|
1671
|
+
filterResult = refreshResult.filter;
|
|
1672
|
+
}
|
|
1673
|
+
if (!blockingPanelClose?.closed) {
|
|
1674
|
+
const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
1675
|
+
panelError.refresh_attempt = compactRefresh;
|
|
1676
|
+
throw panelError;
|
|
1677
|
+
}
|
|
1019
1678
|
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
1020
1679
|
rootState = await ensureRecommendViewport(rootState, "recover_after");
|
|
1021
1680
|
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
@@ -1023,25 +1682,25 @@ export async function runRecommendWorkflow({
|
|
|
1023
1682
|
intervalMs: 300
|
|
1024
1683
|
});
|
|
1025
1684
|
resetInfiniteListForRefreshRound(listState, {
|
|
1026
|
-
reason: `context_recovery:${reason}`,
|
|
1027
|
-
round: contextRecoveryAttempts,
|
|
1028
|
-
method: refreshResult.method,
|
|
1029
|
-
metadata: {
|
|
1030
|
-
card_count: cardNodeIds.length,
|
|
1031
|
-
forced_recent_not_view: effectiveForceRecentNotView,
|
|
1032
|
-
counters: countRecommendResultStatuses(results, { greetCount })
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
listEndReason = "";
|
|
1036
|
-
updateRecommendProgress({
|
|
1037
|
-
card_count: cardNodeIds.length,
|
|
1038
|
-
refresh_method: refreshResult.method || null,
|
|
1039
|
-
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
1040
|
-
recovery_reason: reason
|
|
1041
|
-
});
|
|
1042
|
-
return refreshResult;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1685
|
+
reason: `context_recovery:${reason}`,
|
|
1686
|
+
round: contextRecoveryAttempts,
|
|
1687
|
+
method: refreshResult.method,
|
|
1688
|
+
metadata: {
|
|
1689
|
+
card_count: cardNodeIds.length,
|
|
1690
|
+
forced_recent_not_view: effectiveForceRecentNotView,
|
|
1691
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
listEndReason = "";
|
|
1695
|
+
updateRecommendProgress({
|
|
1696
|
+
card_count: cardNodeIds.length,
|
|
1697
|
+
refresh_method: refreshResult.method || null,
|
|
1698
|
+
refresh_forced_recent_not_view: effectiveForceRecentNotView,
|
|
1699
|
+
recovery_reason: reason
|
|
1700
|
+
});
|
|
1701
|
+
return refreshResult;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1045
1704
|
runControl.setPhase("recommend:cleanup");
|
|
1046
1705
|
await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
1047
1706
|
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
|
|
@@ -1088,55 +1747,55 @@ export async function runRecommendWorkflow({
|
|
|
1088
1747
|
throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
|
|
1089
1748
|
}
|
|
1090
1749
|
rootState = await getRecommendRoots(client);
|
|
1091
|
-
rootState = await ensureRecommendViewport(rootState, "page_scope");
|
|
1092
|
-
runControl.checkpoint({
|
|
1093
|
-
page_scope: compactPageScopeSelection(pageScopeSelection)
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
const initialFilterStages = await applyRecommendFilterEnvelopeStages(normalizedFilter, {
|
|
1097
|
-
applyCurrentCityOnly: async () => {
|
|
1098
|
-
await runControl.waitIfPaused();
|
|
1099
|
-
runControl.throwIfCanceled();
|
|
1100
|
-
runControl.setPhase("recommend:current-city-only");
|
|
1101
|
-
const result = await ensureRecommendCurrentCityOnly(
|
|
1102
|
-
client,
|
|
1103
|
-
rootState.iframe.documentNodeId,
|
|
1104
|
-
{ enabled: normalizedFilter.currentCityOnly }
|
|
1105
|
-
);
|
|
1106
|
-
rootState = await getRecommendRoots(client);
|
|
1107
|
-
rootState = await ensureRecommendViewport(rootState, "current_city_only");
|
|
1108
|
-
runControl.checkpoint({
|
|
1109
|
-
current_city_only: compactCurrentCityOnlyResult(result)
|
|
1110
|
-
});
|
|
1111
|
-
return result;
|
|
1112
|
-
},
|
|
1113
|
-
applyFilterPanel: async () => {
|
|
1114
|
-
await runControl.waitIfPaused();
|
|
1115
|
-
runControl.throwIfCanceled();
|
|
1116
|
-
runControl.setPhase("recommend:filter");
|
|
1117
|
-
const result = await selectAndConfirmFirstSafeFilter(
|
|
1118
|
-
client,
|
|
1119
|
-
rootState.iframe.documentNodeId,
|
|
1120
|
-
buildRecommendFilterSelectionOptions(normalizedFilter)
|
|
1121
|
-
);
|
|
1122
|
-
if (!result.confirmed) {
|
|
1123
|
-
throw new Error("Recommend run filter selection was not confirmed");
|
|
1124
|
-
}
|
|
1125
|
-
rootState = await getRecommendRoots(client);
|
|
1126
|
-
rootState = await ensureRecommendViewport(rootState, "filter");
|
|
1127
|
-
runControl.checkpoint({
|
|
1128
|
-
filter: compactFilterResult(result)
|
|
1129
|
-
});
|
|
1130
|
-
return result;
|
|
1131
|
-
}
|
|
1132
|
-
});
|
|
1133
|
-
currentCityOnlyResult = initialFilterStages.current_city_only;
|
|
1134
|
-
filterResult = initialFilterStages.filter;
|
|
1135
|
-
|
|
1136
|
-
await runControl.waitIfPaused();
|
|
1137
|
-
runControl.throwIfCanceled();
|
|
1138
|
-
runControl.setPhase("recommend:cards");
|
|
1139
|
-
rootState = await ensureRecommendViewport(rootState, "cards");
|
|
1750
|
+
rootState = await ensureRecommendViewport(rootState, "page_scope");
|
|
1751
|
+
runControl.checkpoint({
|
|
1752
|
+
page_scope: compactPageScopeSelection(pageScopeSelection)
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
const initialFilterStages = await applyRecommendFilterEnvelopeStages(normalizedFilter, {
|
|
1756
|
+
applyCurrentCityOnly: async () => {
|
|
1757
|
+
await runControl.waitIfPaused();
|
|
1758
|
+
runControl.throwIfCanceled();
|
|
1759
|
+
runControl.setPhase("recommend:current-city-only");
|
|
1760
|
+
const result = await ensureRecommendCurrentCityOnly(
|
|
1761
|
+
client,
|
|
1762
|
+
rootState.iframe.documentNodeId,
|
|
1763
|
+
{ enabled: normalizedFilter.currentCityOnly }
|
|
1764
|
+
);
|
|
1765
|
+
rootState = await getRecommendRoots(client);
|
|
1766
|
+
rootState = await ensureRecommendViewport(rootState, "current_city_only");
|
|
1767
|
+
runControl.checkpoint({
|
|
1768
|
+
current_city_only: compactCurrentCityOnlyResult(result)
|
|
1769
|
+
});
|
|
1770
|
+
return result;
|
|
1771
|
+
},
|
|
1772
|
+
applyFilterPanel: async () => {
|
|
1773
|
+
await runControl.waitIfPaused();
|
|
1774
|
+
runControl.throwIfCanceled();
|
|
1775
|
+
runControl.setPhase("recommend:filter");
|
|
1776
|
+
const result = await selectAndConfirmFirstSafeFilter(
|
|
1777
|
+
client,
|
|
1778
|
+
rootState.iframe.documentNodeId,
|
|
1779
|
+
buildRecommendFilterSelectionOptions(normalizedFilter)
|
|
1780
|
+
);
|
|
1781
|
+
if (!result.confirmed) {
|
|
1782
|
+
throw new Error("Recommend run filter selection was not confirmed");
|
|
1783
|
+
}
|
|
1784
|
+
rootState = await getRecommendRoots(client);
|
|
1785
|
+
rootState = await ensureRecommendViewport(rootState, "filter");
|
|
1786
|
+
runControl.checkpoint({
|
|
1787
|
+
filter: compactFilterResult(result)
|
|
1788
|
+
});
|
|
1789
|
+
return result;
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
currentCityOnlyResult = initialFilterStages.current_city_only;
|
|
1793
|
+
filterResult = initialFilterStages.filter;
|
|
1794
|
+
|
|
1795
|
+
await runControl.waitIfPaused();
|
|
1796
|
+
runControl.throwIfCanceled();
|
|
1797
|
+
runControl.setPhase("recommend:cards");
|
|
1798
|
+
rootState = await ensureRecommendViewport(rootState, "cards");
|
|
1140
1799
|
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
1141
1800
|
timeoutMs: cardTimeoutMs,
|
|
1142
1801
|
intervalMs: 300
|
|
@@ -1149,50 +1808,308 @@ export async function runRecommendWorkflow({
|
|
|
1149
1808
|
list_end_reason: null
|
|
1150
1809
|
});
|
|
1151
1810
|
|
|
1152
|
-
while (countPassedResults(results) < targetPassCount) {
|
|
1153
|
-
const candidateStarted = Date.now();
|
|
1154
|
-
const timings = {};
|
|
1811
|
+
while (countPassedResults(results) < targetPassCount) {
|
|
1812
|
+
const candidateStarted = Date.now();
|
|
1813
|
+
const timings = {};
|
|
1155
1814
|
await runControl.waitIfPaused();
|
|
1156
|
-
runControl.throwIfCanceled();
|
|
1157
|
-
runControl.setPhase("recommend:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1815
|
+
runControl.throwIfCanceled();
|
|
1816
|
+
runControl.setPhase("recommend:list-read");
|
|
1817
|
+
currentDomOperation = "candidate:list-read";
|
|
1818
|
+
const debugBoundaryAction = debugBoundary.take(results.length);
|
|
1819
|
+
let debugForcedListEnd = false;
|
|
1820
|
+
if (debugBoundaryAction) {
|
|
1821
|
+
const debugProgress = {
|
|
1822
|
+
debug_boundary_mode: debugBoundaryAction.mode,
|
|
1823
|
+
debug_boundary_threshold: debugBoundaryAction.threshold,
|
|
1824
|
+
debug_boundary_triggered: true,
|
|
1825
|
+
debug_boundary_trigger_count: debugBoundaryAction.trigger_count,
|
|
1826
|
+
debug_boundary_processed: debugBoundaryAction.processed
|
|
1827
|
+
};
|
|
1828
|
+
updateRecommendProgress(debugProgress);
|
|
1829
|
+
runControl.checkpoint({
|
|
1830
|
+
debug_boundary: {
|
|
1831
|
+
...debugProgress,
|
|
1832
|
+
field: debugBoundaryAction.field,
|
|
1833
|
+
triggered_at: new Date().toISOString()
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
if (debugBoundaryAction.mode === "context_recovery") {
|
|
1837
|
+
runControl.setPhase("recommend:debug-force-context-recovery");
|
|
1838
|
+
await recoverAndReapplyRecommendContext("debug_force_context_recovery_after_processed", null, {
|
|
1839
|
+
forceRecentNotView: true
|
|
1840
|
+
});
|
|
1841
|
+
updateRecommendProgress({
|
|
1842
|
+
debug_force_context_recovery_count: debugBoundaryAction.trigger_count
|
|
1176
1843
|
});
|
|
1177
|
-
|
|
1178
|
-
|
|
1844
|
+
runControl.setPhase("recommend:list-read");
|
|
1845
|
+
} else if (debugBoundaryAction.mode === "cdp_reconnect") {
|
|
1846
|
+
debugForceReconnectPending = debugBoundaryAction;
|
|
1847
|
+
updateRecommendProgress({
|
|
1848
|
+
debug_force_cdp_reconnect_pending: true
|
|
1849
|
+
});
|
|
1850
|
+
} else if (debugBoundaryAction.mode === "list_end") {
|
|
1851
|
+
debugForcedListEnd = true;
|
|
1852
|
+
updateRecommendProgress({
|
|
1853
|
+
debug_force_list_end_count: debugBoundaryAction.trigger_count
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const listReadAcquisition = await acquireRecommendListReadWithStaleRecovery({
|
|
1858
|
+
maxRetries: 2,
|
|
1859
|
+
acquire: async () => {
|
|
1860
|
+
await runControl.waitIfPaused();
|
|
1861
|
+
runControl.throwIfCanceled();
|
|
1862
|
+
runControl.setPhase("recommend:list-read");
|
|
1863
|
+
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
1864
|
+
if (debugForcedListEnd) {
|
|
1865
|
+
return {
|
|
1866
|
+
ok: false,
|
|
1867
|
+
end_reached: true,
|
|
1868
|
+
reason: "debug_forced_list_end"
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
return measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
1872
|
+
client,
|
|
1873
|
+
state: listState,
|
|
1874
|
+
maxScrolls: listMaxScrolls,
|
|
1875
|
+
stableSignatureLimit: listStableSignatureLimit,
|
|
1876
|
+
wheelDeltaY: listWheelDeltaY,
|
|
1877
|
+
settleMs: listSettleMs,
|
|
1878
|
+
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
1879
|
+
fallbackPoint: listFallbackResolver,
|
|
1880
|
+
findNodeIds: async () => {
|
|
1881
|
+
currentDomOperation = "candidate:list-find-nodes";
|
|
1882
|
+
let currentRootState = await getRecommendRoots(client);
|
|
1883
|
+
currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
|
|
1884
|
+
rootState = currentRootState;
|
|
1885
|
+
if (debugForceReconnectPending) {
|
|
1886
|
+
runControl.setPhase("recommend:debug-force-cdp-reconnect");
|
|
1887
|
+
const rawClient = client.__rawClient;
|
|
1888
|
+
if (!rawClient || typeof rawClient.close !== "function") {
|
|
1889
|
+
const error = new Error("Guarded CDP client does not expose a closable __rawClient");
|
|
1890
|
+
error.code = "RECOMMEND_DEBUG_RAW_CDP_UNAVAILABLE";
|
|
1891
|
+
throw error;
|
|
1892
|
+
}
|
|
1893
|
+
const forcedReconnectAction = debugForceReconnectPending;
|
|
1894
|
+
debugForceReconnectPending = null;
|
|
1895
|
+
await rawClient.close();
|
|
1896
|
+
runControl.checkpoint({
|
|
1897
|
+
debug_cdp_reconnect: {
|
|
1898
|
+
raw_connection_closed: true,
|
|
1899
|
+
processed: forcedReconnectAction.processed,
|
|
1900
|
+
threshold: forcedReconnectAction.threshold,
|
|
1901
|
+
closed_at: new Date().toISOString()
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
updateRecommendProgress({
|
|
1905
|
+
debug_force_cdp_reconnect_pending: false,
|
|
1906
|
+
debug_force_cdp_reconnect_count: forcedReconnectAction.trigger_count
|
|
1907
|
+
});
|
|
1908
|
+
runControl.setPhase("recommend:list-read");
|
|
1909
|
+
}
|
|
1910
|
+
const currentCardNodeIds = await waitForRecommendCardNodeIds(
|
|
1911
|
+
client,
|
|
1912
|
+
currentRootState.iframe.documentNodeId,
|
|
1913
|
+
{
|
|
1914
|
+
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
1915
|
+
intervalMs: 300
|
|
1916
|
+
}
|
|
1917
|
+
);
|
|
1918
|
+
cardNodeIds = currentCardNodeIds;
|
|
1919
|
+
return currentCardNodeIds;
|
|
1920
|
+
},
|
|
1921
|
+
readCandidate: async (nodeId, { visibleIndex }) => {
|
|
1922
|
+
currentDomOperation = "candidate:list-read-card";
|
|
1923
|
+
return readRecommendCardCandidate(client, nodeId, {
|
|
1924
|
+
targetUrl,
|
|
1925
|
+
source: "recommend-run-card",
|
|
1926
|
+
metadata: {
|
|
1927
|
+
run_candidate_index: results.length,
|
|
1928
|
+
visible_index: visibleIndex
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
},
|
|
1932
|
+
shouldRethrowReadError: isStaleRecommendNodeError,
|
|
1933
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => {
|
|
1934
|
+
currentDomOperation = "candidate:list-bottom-probe";
|
|
1935
|
+
return detectInfiniteListBottomMarker(client, {
|
|
1936
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
1937
|
+
markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
1938
|
+
refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
|
|
1939
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
1940
|
+
maxTextScanNodes: 500
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}));
|
|
1944
|
+
},
|
|
1945
|
+
onStale: async ({ error, diagnostic }) => {
|
|
1946
|
+
listReadStaleRecoveryAttempts += 1;
|
|
1947
|
+
const forensic = checkpointDomStaleForensic(error, {
|
|
1948
|
+
phase: "recommend:list-read",
|
|
1949
|
+
operation: currentDomOperation,
|
|
1950
|
+
candidateIndex: results.length
|
|
1951
|
+
});
|
|
1952
|
+
if (forensic) {
|
|
1953
|
+
diagnostic.forensic_event_id = forensic.event_id;
|
|
1954
|
+
diagnostic.pre_recovery_roots = forensic.pre_recovery_roots;
|
|
1955
|
+
diagnostic.failing_candidate = forensic.candidate;
|
|
1956
|
+
}
|
|
1957
|
+
lastListReadStaleDiagnostic = diagnostic;
|
|
1958
|
+
listReadStaleDiagnostics.push(diagnostic);
|
|
1959
|
+
runControl.checkpoint({
|
|
1960
|
+
list_read_stale_recovery: {
|
|
1961
|
+
diagnostic,
|
|
1962
|
+
recent_diagnostics: listReadStaleDiagnostics.slice(-12),
|
|
1963
|
+
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
1964
|
+
candidate_list: compactInfiniteListState(listState)
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
updateRecommendProgress();
|
|
1179
1968
|
},
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1969
|
+
recover: async ({ error, diagnostic }) => {
|
|
1970
|
+
try {
|
|
1971
|
+
return await recoverRecommendListReadStaleContext({
|
|
1972
|
+
staleAttempt: diagnostic.attempt,
|
|
1973
|
+
listState,
|
|
1974
|
+
rootReacquire: async () => {
|
|
1975
|
+
await runControl.waitIfPaused();
|
|
1976
|
+
runControl.throwIfCanceled();
|
|
1977
|
+
runControl.setPhase("recommend:list-read-root-reacquire");
|
|
1978
|
+
let freshRootState = await getRecommendRoots(client);
|
|
1979
|
+
freshRootState = await ensureRecommendViewport(
|
|
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,
|
|
2008
|
+
{ forceRecentNotView: true }
|
|
2009
|
+
);
|
|
2010
|
+
return {
|
|
2011
|
+
ok: Boolean(recovery?.ok),
|
|
2012
|
+
method: recovery?.method || null,
|
|
2013
|
+
card_count: recovery?.card_count || 0,
|
|
2014
|
+
root_identity: compactRecommendDomRootIdentity(
|
|
2015
|
+
rootState,
|
|
2016
|
+
currentConnectionEpoch()
|
|
2017
|
+
)
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
} catch (recoveryError) {
|
|
2022
|
+
const forensic = domStaleForensics.find((item) => (
|
|
2023
|
+
item.event_id === diagnostic.forensic_event_id
|
|
2024
|
+
));
|
|
2025
|
+
checkpointDomStaleRecovery(forensic, {
|
|
2026
|
+
status: "recovery_failed",
|
|
2027
|
+
recoveryError
|
|
2028
|
+
});
|
|
2029
|
+
updateRecommendProgress({
|
|
2030
|
+
list_read_stale_recovery_failed: true
|
|
2031
|
+
});
|
|
2032
|
+
runControl.checkpoint({
|
|
2033
|
+
list_read_stale_recovery_failed: {
|
|
2034
|
+
trigger: lastListReadStaleDiagnostic,
|
|
2035
|
+
recent_diagnostics: listReadStaleDiagnostics.slice(-12),
|
|
2036
|
+
recovery_error: compactError(recoveryError, "RECOMMEND_LIST_READ_RECOVERY_FAILED"),
|
|
2037
|
+
candidate_list: compactInfiniteListState(listState)
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
throw recoveryError;
|
|
1186
2041
|
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
2042
|
+
},
|
|
2043
|
+
onRecoveryApplied: async ({ diagnostic, recoveryResult }) => {
|
|
2044
|
+
listReadStaleRecoveryApplied += 1;
|
|
2045
|
+
lastListReadStaleDiagnostic = diagnostic;
|
|
2046
|
+
lastListReadRecoveryMode = diagnostic.recovery_mode || null;
|
|
2047
|
+
const forensic = domStaleForensics.find((item) => (
|
|
2048
|
+
item.event_id === diagnostic.forensic_event_id
|
|
2049
|
+
));
|
|
2050
|
+
checkpointDomStaleRecovery(forensic, {
|
|
2051
|
+
status: "recovery_applied",
|
|
2052
|
+
recoveryResult
|
|
2053
|
+
});
|
|
2054
|
+
updateRecommendProgress();
|
|
2055
|
+
runControl.checkpoint({
|
|
2056
|
+
list_read_stale_recovery_applied: {
|
|
2057
|
+
diagnostic,
|
|
2058
|
+
recovery_applied_count: listReadStaleRecoveryApplied,
|
|
2059
|
+
candidate_list: compactInfiniteListState(listState)
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
},
|
|
2063
|
+
onRecovered: async ({ diagnostic }) => {
|
|
2064
|
+
listReadStaleRecoveries += 1;
|
|
2065
|
+
lastListReadStaleDiagnostic = diagnostic;
|
|
2066
|
+
const forensic = domStaleForensics.find((item) => (
|
|
2067
|
+
item.event_id === diagnostic.forensic_event_id
|
|
2068
|
+
));
|
|
2069
|
+
checkpointDomStaleRecovery(forensic, {
|
|
2070
|
+
status: "recovered",
|
|
2071
|
+
recoveryResult: {
|
|
2072
|
+
recovery_mode: diagnostic.recovery_mode,
|
|
2073
|
+
ok: true
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
updateRecommendProgress();
|
|
2077
|
+
runControl.checkpoint({
|
|
2078
|
+
list_read_stale_recovered: {
|
|
2079
|
+
diagnostic,
|
|
2080
|
+
recovery_count: listReadStaleRecoveries,
|
|
2081
|
+
candidate_list: compactInfiniteListState(listState)
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
},
|
|
2085
|
+
onExhausted: async ({ error, diagnostic }) => {
|
|
2086
|
+
listReadStaleRecoveryAttempts += 1;
|
|
2087
|
+
const forensic = checkpointDomStaleForensic(error, {
|
|
2088
|
+
phase: "recommend:list-read",
|
|
2089
|
+
operation: currentDomOperation,
|
|
2090
|
+
candidateIndex: results.length
|
|
2091
|
+
});
|
|
2092
|
+
if (forensic) {
|
|
2093
|
+
diagnostic.forensic_event_id = forensic.event_id;
|
|
2094
|
+
diagnostic.pre_recovery_roots = forensic.pre_recovery_roots;
|
|
2095
|
+
diagnostic.failing_candidate = forensic.candidate;
|
|
2096
|
+
checkpointDomStaleRecovery(forensic, { status: "exhausted" });
|
|
2097
|
+
}
|
|
2098
|
+
lastListReadStaleDiagnostic = diagnostic;
|
|
2099
|
+
listReadStaleDiagnostics.push(diagnostic);
|
|
2100
|
+
updateRecommendProgress({
|
|
2101
|
+
list_read_stale_recovery_exhausted: true
|
|
2102
|
+
});
|
|
2103
|
+
runControl.checkpoint({
|
|
2104
|
+
list_read_stale_recovery_exhausted: {
|
|
2105
|
+
diagnostic,
|
|
2106
|
+
recent_diagnostics: listReadStaleDiagnostics.slice(-12),
|
|
2107
|
+
candidate_list: compactInfiniteListState(listState)
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
const nextCandidateResult = listReadAcquisition.result;
|
|
1196
2113
|
if (!nextCandidateResult.ok) {
|
|
1197
2114
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
1198
2115
|
if (
|
|
@@ -1202,57 +2119,79 @@ export async function runRecommendWorkflow({
|
|
|
1202
2119
|
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
1203
2120
|
) {
|
|
1204
2121
|
await runControl.waitIfPaused();
|
|
1205
|
-
runControl.throwIfCanceled();
|
|
1206
|
-
runControl.setPhase("recommend:refresh");
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
2122
|
+
runControl.throwIfCanceled();
|
|
2123
|
+
runControl.setPhase("recommend:refresh");
|
|
2124
|
+
currentDomOperation = "candidate:list-end-refresh";
|
|
2125
|
+
refreshRounds += 1;
|
|
2126
|
+
let refreshResult;
|
|
2127
|
+
try {
|
|
2128
|
+
refreshResult = await refreshRecommendListAtEnd(client, {
|
|
2129
|
+
rootState,
|
|
2130
|
+
jobLabel,
|
|
2131
|
+
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
2132
|
+
fallbackPageScope: normalizedFallbackPageScope,
|
|
2133
|
+
filter: normalizedFilter,
|
|
2134
|
+
preferEndRefreshButton: debugForcedListEnd ? false : undefined,
|
|
2135
|
+
forceRecentNotView: normalizedFilter.enabled,
|
|
2136
|
+
cardTimeoutMs,
|
|
2137
|
+
buttonSettleMs: refreshButtonSettleMs,
|
|
2138
|
+
reloadSettleMs: refreshReloadSettleMs
|
|
2139
|
+
});
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
checkpointDomStaleForensic(error, {
|
|
2142
|
+
phase: "recommend:refresh",
|
|
2143
|
+
operation: currentDomOperation,
|
|
2144
|
+
candidateIndex: results.length
|
|
2145
|
+
});
|
|
2146
|
+
throw error;
|
|
2147
|
+
}
|
|
2148
|
+
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
1220
2149
|
refreshAttempts.push(compactRefresh);
|
|
1221
2150
|
runControl.checkpoint({
|
|
1222
2151
|
refresh_round: refreshRounds,
|
|
1223
|
-
refresh: compactRefresh
|
|
1224
|
-
});
|
|
1225
|
-
updateRecommendProgress({
|
|
1226
|
-
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
1227
|
-
refresh_method: refreshResult.method || null,
|
|
1228
|
-
refresh_forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view),
|
|
1229
|
-
list_end_reason: listEndReason
|
|
1230
|
-
});
|
|
2152
|
+
refresh: compactRefresh
|
|
2153
|
+
});
|
|
2154
|
+
updateRecommendProgress({
|
|
2155
|
+
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
2156
|
+
refresh_method: refreshResult.method || null,
|
|
2157
|
+
refresh_forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view),
|
|
2158
|
+
list_end_reason: listEndReason
|
|
2159
|
+
});
|
|
1231
2160
|
if (refreshResult.ok) {
|
|
1232
|
-
if (refreshResult.current_city_only) {
|
|
1233
|
-
currentCityOnlyResult = refreshResult.current_city_only;
|
|
1234
|
-
}
|
|
1235
|
-
if (refreshResult.filter) {
|
|
1236
|
-
filterResult = refreshResult.filter;
|
|
2161
|
+
if (refreshResult.current_city_only) {
|
|
2162
|
+
currentCityOnlyResult = refreshResult.current_city_only;
|
|
2163
|
+
}
|
|
2164
|
+
if (refreshResult.filter) {
|
|
2165
|
+
filterResult = refreshResult.filter;
|
|
2166
|
+
}
|
|
2167
|
+
try {
|
|
2168
|
+
currentDomOperation = "candidate:list-end-refresh-reacquire";
|
|
2169
|
+
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
2170
|
+
rootState = await ensureRecommendViewport(rootState, "refresh_after");
|
|
2171
|
+
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
2172
|
+
timeoutMs: cardTimeoutMs,
|
|
2173
|
+
intervalMs: 300
|
|
2174
|
+
});
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
checkpointDomStaleForensic(error, {
|
|
2177
|
+
phase: "recommend:refresh",
|
|
2178
|
+
operation: currentDomOperation,
|
|
2179
|
+
candidateIndex: results.length
|
|
2180
|
+
});
|
|
2181
|
+
throw error;
|
|
1237
2182
|
}
|
|
1238
|
-
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
1239
|
-
rootState = await ensureRecommendViewport(rootState, "refresh_after");
|
|
1240
|
-
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
1241
|
-
timeoutMs: cardTimeoutMs,
|
|
1242
|
-
intervalMs: 300
|
|
1243
|
-
});
|
|
1244
2183
|
resetInfiniteListForRefreshRound(listState, {
|
|
1245
|
-
reason: listEndReason,
|
|
1246
|
-
round: refreshRounds,
|
|
1247
|
-
method: refreshResult.method,
|
|
1248
|
-
metadata: {
|
|
1249
|
-
card_count: cardNodeIds.length,
|
|
1250
|
-
forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view)
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
listEndReason = "";
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
2184
|
+
reason: listEndReason,
|
|
2185
|
+
round: refreshRounds,
|
|
2186
|
+
method: refreshResult.method,
|
|
2187
|
+
metadata: {
|
|
2188
|
+
card_count: cardNodeIds.length,
|
|
2189
|
+
forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view)
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
listEndReason = "";
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
1256
2195
|
throw createRecommendRefreshFailureError(compactRefresh, {
|
|
1257
2196
|
listEndReason,
|
|
1258
2197
|
targetCount: targetPassCount,
|
|
@@ -1262,6 +2201,7 @@ export async function runRecommendWorkflow({
|
|
|
1262
2201
|
break;
|
|
1263
2202
|
}
|
|
1264
2203
|
|
|
2204
|
+
runControl.setPhase("recommend:candidate");
|
|
1265
2205
|
const index = results.length;
|
|
1266
2206
|
let cardNodeId = nextCandidateResult.item.node_id;
|
|
1267
2207
|
const candidateKey = nextCandidateResult.item.key;
|
|
@@ -1276,11 +2216,13 @@ export async function runRecommendWorkflow({
|
|
|
1276
2216
|
if (index < effectiveDetailLimit) {
|
|
1277
2217
|
try {
|
|
1278
2218
|
await runControl.waitIfPaused();
|
|
1279
|
-
runControl.throwIfCanceled();
|
|
1280
|
-
runControl.setPhase("recommend:detail");
|
|
1281
|
-
detailStep = "ensure_viewport";
|
|
1282
|
-
|
|
1283
|
-
|
|
2219
|
+
runControl.throwIfCanceled();
|
|
2220
|
+
runControl.setPhase("recommend:detail");
|
|
2221
|
+
detailStep = "ensure_viewport";
|
|
2222
|
+
currentDomOperation = "candidate:detail-ensure-viewport";
|
|
2223
|
+
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
2224
|
+
currentDomOperation = "candidate:detail-close-blocking-panels";
|
|
2225
|
+
const blockingPanelClose = await closeRecommendBlockingPanels(client, {
|
|
1284
2226
|
attemptsLimit: 2,
|
|
1285
2227
|
rootState
|
|
1286
2228
|
});
|
|
@@ -1298,10 +2240,12 @@ export async function runRecommendWorkflow({
|
|
|
1298
2240
|
}
|
|
1299
2241
|
if (blockingPanelClose.already_closed === false) {
|
|
1300
2242
|
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1301
|
-
}
|
|
1302
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1303
|
-
detailStep = "open_detail";
|
|
1304
|
-
|
|
2243
|
+
}
|
|
2244
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2245
|
+
detailStep = "open_detail";
|
|
2246
|
+
currentDomOperation = "candidate:detail-open";
|
|
2247
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2248
|
+
networkRecorder.clear();
|
|
1305
2249
|
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
1306
2250
|
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
1307
2251
|
cardNodeId,
|
|
@@ -1317,8 +2261,10 @@ export async function runRecommendWorkflow({
|
|
|
1317
2261
|
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
1318
2262
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
1319
2263
|
screeningCandidate = cardCandidate;
|
|
1320
|
-
if (shouldSkipRecentColleagueContacted) {
|
|
1321
|
-
detailStep = "check_colleague_contact";
|
|
2264
|
+
if (shouldSkipRecentColleagueContacted) {
|
|
2265
|
+
detailStep = "check_colleague_contact";
|
|
2266
|
+
currentDomOperation = "candidate:detail-colleague-contact";
|
|
2267
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1322
2268
|
try {
|
|
1323
2269
|
colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
|
|
1324
2270
|
client,
|
|
@@ -1356,9 +2302,11 @@ export async function runRecommendWorkflow({
|
|
|
1356
2302
|
};
|
|
1357
2303
|
}
|
|
1358
2304
|
}
|
|
1359
|
-
if (!skipRecentColleagueContact) {
|
|
1360
|
-
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
1361
|
-
detailStep = "wait_network";
|
|
2305
|
+
if (!skipRecentColleagueContact) {
|
|
2306
|
+
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
2307
|
+
detailStep = "wait_network";
|
|
2308
|
+
currentDomOperation = "candidate:detail-network-wait";
|
|
2309
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1362
2310
|
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
1363
2311
|
waitForRecommendDetailNetworkEvents,
|
|
1364
2312
|
networkRecorder,
|
|
@@ -1371,8 +2319,10 @@ export async function runRecommendWorkflow({
|
|
|
1371
2319
|
));
|
|
1372
2320
|
if (networkWait?.elapsed_ms != null) {
|
|
1373
2321
|
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
1374
|
-
}
|
|
1375
|
-
detailStep = "extract_detail";
|
|
2322
|
+
}
|
|
2323
|
+
detailStep = "extract_detail";
|
|
2324
|
+
currentDomOperation = "candidate:detail-extract";
|
|
2325
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1376
2326
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
1377
2327
|
cardCandidate,
|
|
1378
2328
|
cardNodeId,
|
|
@@ -1396,8 +2346,10 @@ export async function runRecommendWorkflow({
|
|
|
1396
2346
|
parsedNetworkProfileCount,
|
|
1397
2347
|
waitResult: networkWait
|
|
1398
2348
|
});
|
|
1399
|
-
} else {
|
|
1400
|
-
detailStep = "wait_capture_target";
|
|
2349
|
+
} else {
|
|
2350
|
+
detailStep = "wait_capture_target";
|
|
2351
|
+
currentDomOperation = "candidate:detail-wait-capture-target";
|
|
2352
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1401
2353
|
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
1402
2354
|
domain: "recommend",
|
|
1403
2355
|
timeoutMs: 6000,
|
|
@@ -1406,70 +2358,206 @@ export async function runRecommendWorkflow({
|
|
|
1406
2358
|
captureTarget = captureTargetWait.target || null;
|
|
1407
2359
|
const captureNodeId = captureTarget?.node_id || null;
|
|
1408
2360
|
if (captureNodeId) {
|
|
1409
|
-
const imageEvidencePath = imageEvidenceFilePath({
|
|
1410
|
-
imageOutputDir,
|
|
1411
|
-
domain: "recommend",
|
|
1412
|
-
runId: runControl?.runId,
|
|
1413
|
-
index,
|
|
1414
|
-
extension: "jpg"
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2361
|
+
const imageEvidencePath = imageEvidenceFilePath({
|
|
2362
|
+
imageOutputDir,
|
|
2363
|
+
domain: "recommend",
|
|
2364
|
+
runId: runControl?.runId,
|
|
2365
|
+
index,
|
|
2366
|
+
extension: "jpg"
|
|
2367
|
+
});
|
|
2368
|
+
const captureImageForTarget = (target, targetWait, resumeCheckpoint = null) => captureScrolledNodeScreenshots(
|
|
2369
|
+
client,
|
|
2370
|
+
target.node_id,
|
|
2371
|
+
{
|
|
2372
|
+
filePath: imageEvidencePath,
|
|
2373
|
+
format: "jpeg",
|
|
2374
|
+
quality: 72,
|
|
2375
|
+
optimize: true,
|
|
2376
|
+
resizeMaxWidth: 1100,
|
|
2377
|
+
captureViewport: false,
|
|
2378
|
+
iframeOwnerNodeId: target?.iframe_node_id || null,
|
|
2379
|
+
padding: 0,
|
|
2380
|
+
maxScreenshots: maxImagePages,
|
|
2381
|
+
wheelDeltaY: imageWheelDeltaY,
|
|
2382
|
+
settleMs: 350,
|
|
2383
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
2384
|
+
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
2385
|
+
stepTimeoutMs: 45000,
|
|
2386
|
+
totalTimeoutMs: 90000,
|
|
2387
|
+
duplicateStopCount: 2,
|
|
2388
|
+
skipDuplicateScreenshots: true,
|
|
2389
|
+
requireTerminalProof: true,
|
|
2390
|
+
composeForLlm: true,
|
|
2391
|
+
llmPagesPerImage: 3,
|
|
2392
|
+
llmResizeMaxWidth: 1100,
|
|
2393
|
+
llmQuality: 72,
|
|
2394
|
+
resumeCheckpoint,
|
|
2395
|
+
metadata: {
|
|
2396
|
+
domain: "recommend",
|
|
2397
|
+
capture_mode: "scroll_sequence",
|
|
2398
|
+
acquisition_reason: "network_miss_image_fallback",
|
|
2399
|
+
run_candidate_index: index,
|
|
2400
|
+
candidate_key: candidateKey,
|
|
2401
|
+
capture_target: target,
|
|
2402
|
+
capture_target_wait: targetWait,
|
|
2403
|
+
resumed_from_checkpoint: Boolean(resumeCheckpoint)
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
);
|
|
2407
|
+
try {
|
|
2408
|
+
detailStep = "capture_image";
|
|
2409
|
+
currentDomOperation = "candidate:detail-capture-image";
|
|
2410
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2411
|
+
imageEvidence = await measureTiming(
|
|
2412
|
+
timings,
|
|
2413
|
+
"screenshot_capture_ms",
|
|
2414
|
+
() => captureImageForTarget(captureTarget, captureTargetWait)
|
|
2415
|
+
);
|
|
2416
|
+
imageEvidence = requireCompleteImageEvidence(imageEvidence, {
|
|
2417
|
+
code: "IMAGE_CAPTURE_EVIDENCE_MISSING",
|
|
2418
|
+
message: "Recommend CV capture returned no complete persisted evidence",
|
|
2419
|
+
metadata: { domain: "recommend", candidate_key: candidateKey }
|
|
2420
|
+
});
|
|
2421
|
+
source = isIncompleteImageEvidence(imageEvidence) ? "image_capture_failed" : "image";
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
if (!isRecoverableImageCaptureError(error)) throw error;
|
|
2424
|
+
const retryReservation = imageCaptureWorkflowRetries.consume(candidateKey);
|
|
2425
|
+
if (retryReservation.allowed) {
|
|
2426
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
2427
|
+
timings.image_capture_workflow_retry = retryReservation;
|
|
2428
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
2429
|
+
const resumeCheckpoint = imageCaptureResumeCheckpoint(error);
|
|
2430
|
+
if (resumeCheckpoint) {
|
|
2431
|
+
detailStep = "resume_image_capture_reacquire";
|
|
2432
|
+
const resumeAttempt = await attemptImageCaptureCheckpointResume({
|
|
2433
|
+
checkpoint: resumeCheckpoint,
|
|
2434
|
+
reacquire: () => reacquireImageCaptureResumeTarget({
|
|
2435
|
+
domain: "recommend",
|
|
2436
|
+
getRoots: () => getRecommendRoots(client),
|
|
2437
|
+
ensureViewport: (freshRoots) => ensureRecommendViewport(
|
|
2438
|
+
freshRoots,
|
|
2439
|
+
"image_capture_resume_reacquire"
|
|
2440
|
+
),
|
|
2441
|
+
getDetailState: () => waitForRecommendDetail(client, {
|
|
2442
|
+
timeoutMs: 4000,
|
|
2443
|
+
intervalMs: 200
|
|
2444
|
+
}),
|
|
2445
|
+
isDetailAvailable: (state) => Boolean(state?.popup || state?.resumeIframe),
|
|
2446
|
+
waitForTarget: (detailState) => waitForCvCaptureTarget(client, detailState, {
|
|
2447
|
+
domain: "recommend",
|
|
2448
|
+
timeoutMs: 4000,
|
|
2449
|
+
intervalMs: 200
|
|
2450
|
+
})
|
|
2451
|
+
}),
|
|
2452
|
+
capture: (reacquired) => {
|
|
2453
|
+
rootState = reacquired.root_state;
|
|
2454
|
+
captureTarget = reacquired.target;
|
|
2455
|
+
captureTargetWait = reacquired.target_wait;
|
|
2456
|
+
detailStep = "resume_image_capture";
|
|
2457
|
+
return measureTiming(
|
|
2458
|
+
timings,
|
|
2459
|
+
"screenshot_capture_ms",
|
|
2460
|
+
() => captureImageForTarget(captureTarget, captureTargetWait, resumeCheckpoint)
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
if (resumeAttempt.outcome === "reacquire_failed") {
|
|
2465
|
+
const reacquireError = resumeAttempt.error;
|
|
2466
|
+
imageEvidence = {
|
|
2467
|
+
...createRecoverableImageCaptureEvidence(reacquireError, {
|
|
2468
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
2469
|
+
filePath: imageEvidencePath,
|
|
2470
|
+
extension: "jpg",
|
|
2471
|
+
maxScreenshots: maxImagePages
|
|
2472
|
+
}),
|
|
2473
|
+
coverage_complete: false,
|
|
2474
|
+
resumed_from_checkpoint: true,
|
|
2475
|
+
resume_checkpoint_id: resumeCheckpoint.checkpoint_id || null,
|
|
2476
|
+
coverage_checkpoint: resumeCheckpoint
|
|
2477
|
+
};
|
|
2478
|
+
source = "image_capture_failed";
|
|
2479
|
+
timings.image_capture_resume = {
|
|
2480
|
+
attempted: true,
|
|
2481
|
+
ok: false,
|
|
2482
|
+
phase: "reacquire",
|
|
2483
|
+
checkpoint_id: resumeCheckpoint.checkpoint_id || null,
|
|
2484
|
+
error: compactError(reacquireError, "IMAGE_CAPTURE_RESUME_REACQUIRE_FAILED")
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
if (resumeAttempt.outcome === "completed") {
|
|
2488
|
+
imageEvidence = requireCompleteImageEvidence(resumeAttempt.evidence, {
|
|
2489
|
+
code: "IMAGE_CAPTURE_RESUME_EVIDENCE_MISSING",
|
|
2490
|
+
message: "Recommend resumed CV capture returned no complete persisted evidence",
|
|
2491
|
+
metadata: { domain: "recommend", candidate_key: candidateKey }
|
|
2492
|
+
});
|
|
2493
|
+
source = isIncompleteImageEvidence(imageEvidence) ? "image_capture_failed" : "image";
|
|
2494
|
+
timings.image_capture_resume = {
|
|
2495
|
+
attempted: true,
|
|
2496
|
+
ok: !isIncompleteImageEvidence(imageEvidence),
|
|
2497
|
+
phase: "capture",
|
|
2498
|
+
checkpoint_id: resumeCheckpoint.checkpoint_id || null,
|
|
2499
|
+
confirmed_screenshot_count: resumeCheckpoint.unique_screenshot_count || 0
|
|
2500
|
+
};
|
|
2501
|
+
} else if (resumeAttempt.outcome === "capture_failed") {
|
|
2502
|
+
const resumeError = resumeAttempt.error;
|
|
2503
|
+
imageEvidence = {
|
|
2504
|
+
...createRecoverableImageCaptureEvidence(resumeError, {
|
|
2505
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
2506
|
+
filePath: imageEvidencePath,
|
|
2507
|
+
extension: "jpg",
|
|
2508
|
+
maxScreenshots: maxImagePages
|
|
2509
|
+
}),
|
|
2510
|
+
coverage_complete: false,
|
|
2511
|
+
resumed_from_checkpoint: true,
|
|
2512
|
+
resume_checkpoint_id: resumeCheckpoint.checkpoint_id || null,
|
|
2513
|
+
coverage_checkpoint: resumeError.capture_checkpoint || resumeCheckpoint
|
|
2514
|
+
};
|
|
2515
|
+
source = "image_capture_failed";
|
|
2516
|
+
timings.image_capture_resume = {
|
|
2517
|
+
attempted: true,
|
|
2518
|
+
ok: false,
|
|
2519
|
+
phase: "capture",
|
|
2520
|
+
checkpoint_id: resumeCheckpoint.checkpoint_id || null,
|
|
2521
|
+
error: compactError(resumeError, "IMAGE_CAPTURE_RESUME_FAILED")
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
} else {
|
|
2525
|
+
imageEvidence = {
|
|
2526
|
+
...createRecoverableImageCaptureEvidence(error, {
|
|
2527
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
2528
|
+
filePath: imageEvidencePath,
|
|
2529
|
+
extension: "jpg",
|
|
2530
|
+
maxScreenshots: maxImagePages
|
|
2531
|
+
}),
|
|
2532
|
+
coverage_complete: false
|
|
2533
|
+
};
|
|
2534
|
+
source = "image_capture_failed";
|
|
2535
|
+
timings.image_capture_resume = {
|
|
2536
|
+
attempted: false,
|
|
2537
|
+
ok: false,
|
|
2538
|
+
phase: "checkpoint",
|
|
2539
|
+
error: {
|
|
2540
|
+
code: "IMAGE_CAPTURE_CHECKPOINT_UNAVAILABLE",
|
|
2541
|
+
message: "Capture failed without a resumable checkpoint"
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
} else {
|
|
2546
|
+
imageEvidence = {
|
|
2547
|
+
...createRecoverableImageCaptureEvidence(error, {
|
|
2548
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
2549
|
+
filePath: imageEvidencePath,
|
|
2550
|
+
extension: "jpg",
|
|
2551
|
+
maxScreenshots: maxImagePages
|
|
2552
|
+
}),
|
|
2553
|
+
coverage_complete: false,
|
|
2554
|
+
metadata: {
|
|
2555
|
+
image_capture_workflow_retry: retryReservation
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
source = "image_capture_failed";
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
1473
2561
|
recordCvImageFallback(cvAcquisitionState, {
|
|
1474
2562
|
reason: source === "image_capture_failed"
|
|
1475
2563
|
? "network_miss_image_capture_failed"
|
|
@@ -1478,9 +2566,18 @@ export async function runRecommendWorkflow({
|
|
|
1478
2566
|
waitResult: networkWait,
|
|
1479
2567
|
imageEvidence
|
|
1480
2568
|
});
|
|
1481
|
-
} else {
|
|
1482
|
-
source = "missing_capture_node";
|
|
1483
|
-
|
|
2569
|
+
} else {
|
|
2570
|
+
source = "missing_capture_node";
|
|
2571
|
+
imageEvidence = createRequiredImageEvidenceFailure({
|
|
2572
|
+
code: "IMAGE_CAPTURE_TARGET_UNAVAILABLE",
|
|
2573
|
+
message: "Recommend CV capture target was unavailable after the network fallback missed",
|
|
2574
|
+
metadata: {
|
|
2575
|
+
domain: "recommend",
|
|
2576
|
+
candidate_key: candidateKey,
|
|
2577
|
+
capture_target_wait: captureTargetWait || null
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
recordCvNetworkMiss(cvAcquisitionState, {
|
|
1484
2581
|
reason: "network_miss_no_capture_node",
|
|
1485
2582
|
parsedNetworkProfileCount,
|
|
1486
2583
|
waitResult: networkWait
|
|
@@ -1501,21 +2598,42 @@ export async function runRecommendWorkflow({
|
|
|
1501
2598
|
};
|
|
1502
2599
|
screeningCandidate = detailResult.candidate;
|
|
1503
2600
|
}
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
1506
|
-
const
|
|
1507
|
-
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
2603
|
+
const staleForensic = checkpointDomStaleForensic(error, {
|
|
2604
|
+
phase: "recommend:detail",
|
|
2605
|
+
operation: currentDomOperation,
|
|
2606
|
+
detailStep,
|
|
2607
|
+
candidateIndex: index,
|
|
2608
|
+
candidateKey,
|
|
2609
|
+
cardNodeId
|
|
2610
|
+
});
|
|
2611
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
2612
|
+
if (recoveryCount < 1) {
|
|
1508
2613
|
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1509
2614
|
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1510
2615
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1511
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1512
|
-
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1513
|
-
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
2616
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2617
|
+
await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
|
|
2618
|
+
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
|
+
}
|
|
2636
|
+
checkpointDomStaleRecovery(staleForensic, { status: "candidate_failed_closed" });
|
|
1519
2637
|
recoverableDetailError = error;
|
|
1520
2638
|
detailResult = null;
|
|
1521
2639
|
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
@@ -1526,11 +2644,12 @@ export async function runRecommendWorkflow({
|
|
|
1526
2644
|
}
|
|
1527
2645
|
|
|
1528
2646
|
await runControl.waitIfPaused();
|
|
1529
|
-
runControl.throwIfCanceled();
|
|
1530
|
-
runControl.setPhase("recommend:screening");
|
|
1531
|
-
let llmResult = null;
|
|
1532
|
-
|
|
1533
|
-
|
|
2647
|
+
runControl.throwIfCanceled();
|
|
2648
|
+
runControl.setPhase("recommend:screening");
|
|
2649
|
+
let llmResult = null;
|
|
2650
|
+
const imageAcquisitionFailed = shouldFailClosedRecommendImageAcquisition(detailResult);
|
|
2651
|
+
if (useLlmScreening) {
|
|
2652
|
+
if (skipRecentColleagueContact || recoverableDetailError || imageAcquisitionFailed) {
|
|
1534
2653
|
llmResult = null;
|
|
1535
2654
|
} else if (!llmConfig) {
|
|
1536
2655
|
llmResult = createMissingLlmConfigResult();
|
|
@@ -1564,47 +2683,85 @@ export async function runRecommendWorkflow({
|
|
|
1564
2683
|
? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
|
|
1565
2684
|
: recoverableDetailError
|
|
1566
2685
|
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
1567
|
-
:
|
|
1568
|
-
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
1569
|
-
code: detailResult
|
|
1570
|
-
message: detailResult
|
|
1571
|
-
})
|
|
2686
|
+
: imageAcquisitionFailed
|
|
2687
|
+
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
2688
|
+
code: detailResult?.image_evidence?.error_code || "IMAGE_CAPTURE_EVIDENCE_MISSING",
|
|
2689
|
+
message: detailResult?.image_evidence?.error || "Required CV image evidence is unavailable"
|
|
2690
|
+
})
|
|
1572
2691
|
: useLlmScreening
|
|
1573
2692
|
? llmResultToScreening(llmResult, screeningCandidate)
|
|
1574
2693
|
: screenCandidate(screeningCandidate, { criteria });
|
|
1575
2694
|
let actionDiscovery = null;
|
|
1576
2695
|
let postActionResult = null;
|
|
1577
|
-
let closeFailureError = null;
|
|
1578
|
-
let closeRecoveryFailure = null;
|
|
1579
|
-
if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2696
|
+
let closeFailureError = null;
|
|
2697
|
+
let closeRecoveryFailure = null;
|
|
2698
|
+
if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
|
|
2699
|
+
try {
|
|
2700
|
+
const postActionStarted = Date.now();
|
|
2701
|
+
await runControl.waitIfPaused();
|
|
2702
|
+
runControl.throwIfCanceled();
|
|
2703
|
+
runControl.setPhase("recommend:post-action");
|
|
2704
|
+
detailStep = "post_action_discovery";
|
|
2705
|
+
currentDomOperation = "candidate:post-action-discovery";
|
|
2706
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2707
|
+
await maybeHumanActionCooldown("before_post_action", timings);
|
|
2708
|
+
actionDiscovery = await waitForRecommendDetailActionControls(client, {
|
|
2709
|
+
timeoutMs: actionTimeoutMs,
|
|
2710
|
+
intervalMs: actionIntervalMs,
|
|
2711
|
+
requireAny: true
|
|
2712
|
+
});
|
|
2713
|
+
detailStep = "post_action_execute";
|
|
2714
|
+
currentDomOperation = "candidate:post-action-execute";
|
|
2715
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2716
|
+
postActionResult = await runRecommendPostAction({
|
|
2717
|
+
client,
|
|
2718
|
+
screening,
|
|
2719
|
+
actionDiscovery,
|
|
2720
|
+
postAction: normalizedPostAction,
|
|
2721
|
+
greetCount,
|
|
2722
|
+
maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
2723
|
+
executePostAction,
|
|
2724
|
+
afterClickDelayMs: actionAfterClickDelayMs
|
|
2725
|
+
});
|
|
2726
|
+
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
2727
|
+
greetCount += 1;
|
|
2728
|
+
}
|
|
2729
|
+
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
checkpointDomStaleForensic(error, {
|
|
2732
|
+
phase: "recommend:post-action",
|
|
2733
|
+
operation: currentDomOperation,
|
|
2734
|
+
detailStep,
|
|
2735
|
+
candidateIndex: index,
|
|
2736
|
+
candidateKey,
|
|
2737
|
+
cardNodeId
|
|
2738
|
+
});
|
|
2739
|
+
throw error;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
if (detailResult && closeDetail && !detailResult.close_result?.closed) {
|
|
2743
|
+
runControl.setPhase("recommend:close-detail");
|
|
2744
|
+
detailStep = "close_detail";
|
|
2745
|
+
currentDomOperation = "candidate:close-detail";
|
|
2746
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
2747
|
+
try {
|
|
2748
|
+
detailResult.close_result = await measureTiming(
|
|
2749
|
+
timings,
|
|
2750
|
+
"close_detail_ms",
|
|
2751
|
+
() => closeRecommendDetail(client)
|
|
2752
|
+
);
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
checkpointDomStaleForensic(error, {
|
|
2755
|
+
phase: "recommend:close-detail",
|
|
2756
|
+
operation: currentDomOperation,
|
|
2757
|
+
detailStep,
|
|
2758
|
+
candidateIndex: index,
|
|
2759
|
+
candidateKey,
|
|
2760
|
+
cardNodeId
|
|
2761
|
+
});
|
|
2762
|
+
throw error;
|
|
2763
|
+
}
|
|
2764
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1608
2765
|
if (!detailResult.close_result?.closed) {
|
|
1609
2766
|
closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
|
|
1610
2767
|
try {
|
|
@@ -1623,17 +2780,17 @@ export async function runRecommendWorkflow({
|
|
|
1623
2780
|
} catch (error) {
|
|
1624
2781
|
closeRecoveryFailure = error;
|
|
1625
2782
|
detailResult.cv_acquisition = {
|
|
1626
|
-
...(detailResult.cv_acquisition || {}),
|
|
1627
|
-
close_recovery: {
|
|
1628
|
-
ok: false,
|
|
1629
|
-
reason: "context_recovery_failed",
|
|
1630
|
-
error: error?.message || String(error),
|
|
1631
|
-
forced_recent_not_view: Boolean(normalizedFilter.enabled)
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
2783
|
+
...(detailResult.cv_acquisition || {}),
|
|
2784
|
+
close_recovery: {
|
|
2785
|
+
ok: false,
|
|
2786
|
+
reason: "context_recovery_failed",
|
|
2787
|
+
error: error?.message || String(error),
|
|
2788
|
+
forced_recent_not_view: Boolean(normalizedFilter.enabled)
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
1637
2794
|
timings.total_ms = Date.now() - candidateStarted;
|
|
1638
2795
|
const compactResult = {
|
|
1639
2796
|
index,
|
|
@@ -1670,9 +2827,10 @@ export async function runRecommendWorkflow({
|
|
|
1670
2827
|
last_candidate_key: candidateKey,
|
|
1671
2828
|
last_score: screening.score
|
|
1672
2829
|
});
|
|
1673
|
-
const checkpointStarted = Date.now();
|
|
1674
|
-
runControl.checkpoint({
|
|
1675
|
-
|
|
2830
|
+
const checkpointStarted = Date.now();
|
|
2831
|
+
runControl.checkpoint({
|
|
2832
|
+
in_progress_candidate: null,
|
|
2833
|
+
results,
|
|
1676
2834
|
last_candidate: {
|
|
1677
2835
|
id: screeningCandidate.id || null,
|
|
1678
2836
|
key: candidateKey,
|
|
@@ -1729,17 +2887,17 @@ export async function runRecommendWorkflow({
|
|
|
1729
2887
|
}
|
|
1730
2888
|
|
|
1731
2889
|
runControl.setPhase("recommend:done");
|
|
1732
|
-
return {
|
|
1733
|
-
domain: "recommend",
|
|
1734
|
-
target_url: targetUrl,
|
|
1735
|
-
job_selection: compactJobSelection(jobSelection),
|
|
1736
|
-
page_scope: compactPageScopeSelection(pageScopeSelection),
|
|
1737
|
-
current_city_only: compactCurrentCityOnlyResult(currentCityOnlyResult),
|
|
1738
|
-
filter: compactFilterResult(filterResult),
|
|
1739
|
-
card_count: cardNodeIds.length,
|
|
1740
|
-
candidate_list: compactInfiniteListState(listState),
|
|
1741
|
-
viewport_health: {
|
|
1742
|
-
stats: viewportGuard.getStats(),
|
|
2890
|
+
return {
|
|
2891
|
+
domain: "recommend",
|
|
2892
|
+
target_url: targetUrl,
|
|
2893
|
+
job_selection: compactJobSelection(jobSelection),
|
|
2894
|
+
page_scope: compactPageScopeSelection(pageScopeSelection),
|
|
2895
|
+
current_city_only: compactCurrentCityOnlyResult(currentCityOnlyResult),
|
|
2896
|
+
filter: compactFilterResult(filterResult),
|
|
2897
|
+
card_count: cardNodeIds.length,
|
|
2898
|
+
candidate_list: compactInfiniteListState(listState),
|
|
2899
|
+
viewport_health: {
|
|
2900
|
+
stats: viewportGuard.getStats(),
|
|
1743
2901
|
events: viewportGuard.getEvents()
|
|
1744
2902
|
},
|
|
1745
2903
|
human_behavior: effectiveHumanBehavior,
|
|
@@ -1749,7 +2907,19 @@ export async function runRecommendWorkflow({
|
|
|
1749
2907
|
refresh_rounds: refreshRounds,
|
|
1750
2908
|
refresh_attempts: refreshAttempts,
|
|
1751
2909
|
context_recoveries: contextRecoveryAttempts,
|
|
1752
|
-
|
|
2910
|
+
debug_boundary: debugBoundary.getState(),
|
|
2911
|
+
list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
|
|
2912
|
+
list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
|
|
2913
|
+
list_read_stale_recoveries: listReadStaleRecoveries,
|
|
2914
|
+
last_list_read_recovery_mode: lastListReadRecoveryMode,
|
|
2915
|
+
last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
|
|
2916
|
+
list_read_stale_diagnostics: listReadStaleDiagnostics.slice(-12),
|
|
2917
|
+
dom_stale_events: domStaleEventCount,
|
|
2918
|
+
dom_stale_forensics: domStaleForensics.slice(-12),
|
|
2919
|
+
dom_lifecycle_timeline: domLifecycleTimeline.slice(-20),
|
|
2920
|
+
...countRecommendResultStatuses(results, { greetCount }),
|
|
2921
|
+
transient_recovered: countRecommendResultStatuses(results, { greetCount }).transient_recovered
|
|
2922
|
+
+ listReadStaleRecoveries,
|
|
1753
2923
|
results
|
|
1754
2924
|
};
|
|
1755
2925
|
}
|
|
@@ -1805,6 +2975,10 @@ export function createRecommendRunService({
|
|
|
1805
2975
|
humanBehavior = null,
|
|
1806
2976
|
skipRecentColleagueContacted = true,
|
|
1807
2977
|
colleagueContactWindowDays = 14,
|
|
2978
|
+
debugTestMode = undefined,
|
|
2979
|
+
debugForceListEndAfterProcessed = undefined,
|
|
2980
|
+
debugForceContextRecoveryAfterProcessed = undefined,
|
|
2981
|
+
debugForceCdpReconnectAfterProcessed = undefined,
|
|
1808
2982
|
name = "recommend-domain-run"
|
|
1809
2983
|
} = {}) {
|
|
1810
2984
|
if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
|
|
@@ -1821,6 +2995,12 @@ export function createRecommendRunService({
|
|
|
1821
2995
|
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1822
2996
|
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1823
2997
|
const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
|
|
2998
|
+
const debugBoundaryOptions = normalizeRecommendDebugBoundaryOptions({
|
|
2999
|
+
debugTestMode: debugTestMode === true,
|
|
3000
|
+
debugForceListEndAfterProcessed: debugForceListEndAfterProcessed ?? null,
|
|
3001
|
+
debugForceContextRecoveryAfterProcessed: debugForceContextRecoveryAfterProcessed ?? null,
|
|
3002
|
+
debugForceCdpReconnectAfterProcessed: debugForceCdpReconnectAfterProcessed ?? null
|
|
3003
|
+
});
|
|
1824
3004
|
return manager.startRun({
|
|
1825
3005
|
runId,
|
|
1826
3006
|
name,
|
|
@@ -1828,17 +3008,17 @@ export function createRecommendRunService({
|
|
|
1828
3008
|
context: {
|
|
1829
3009
|
domain: "recommend",
|
|
1830
3010
|
target_url: targetUrl,
|
|
1831
|
-
criteria_present: Boolean(criteria),
|
|
1832
|
-
job_label: jobLabel || "",
|
|
1833
|
-
requested_page_scope: requestedPageScope,
|
|
1834
|
-
fallback_page_scope: normalizedFallbackPageScope,
|
|
1835
|
-
filter: normalizedFilter,
|
|
1836
|
-
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
1837
|
-
max_candidates: maxCandidates,
|
|
1838
|
-
max_candidates_semantics: "passed_candidates",
|
|
1839
|
-
detail_limit: normalizedDetailLimit,
|
|
1840
|
-
close_detail: closeDetail,
|
|
1841
|
-
cv_acquisition_mode: cvAcquisitionMode,
|
|
3011
|
+
criteria_present: Boolean(criteria),
|
|
3012
|
+
job_label: jobLabel || "",
|
|
3013
|
+
requested_page_scope: requestedPageScope,
|
|
3014
|
+
fallback_page_scope: normalizedFallbackPageScope,
|
|
3015
|
+
filter: normalizedFilter,
|
|
3016
|
+
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
3017
|
+
max_candidates: maxCandidates,
|
|
3018
|
+
max_candidates_semantics: "passed_candidates",
|
|
3019
|
+
detail_limit: normalizedDetailLimit,
|
|
3020
|
+
close_detail: closeDetail,
|
|
3021
|
+
cv_acquisition_mode: cvAcquisitionMode,
|
|
1842
3022
|
max_image_pages: maxImagePages,
|
|
1843
3023
|
image_wheel_delta_y: imageWheelDeltaY,
|
|
1844
3024
|
list_max_scrolls: listMaxScrolls,
|
|
@@ -1866,7 +3046,10 @@ export function createRecommendRunService({
|
|
|
1866
3046
|
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1867
3047
|
human_behavior: effectiveHumanBehavior,
|
|
1868
3048
|
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1869
|
-
human_rest_enabled: effectiveHumanRestEnabled
|
|
3049
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
3050
|
+
debug_test_mode: debugBoundaryOptions.debugTestMode,
|
|
3051
|
+
debug_boundary_mode: debugBoundaryOptions.mode,
|
|
3052
|
+
debug_boundary_threshold: debugBoundaryOptions.threshold
|
|
1870
3053
|
},
|
|
1871
3054
|
progress: {
|
|
1872
3055
|
card_count: 0,
|
|
@@ -1882,21 +3065,25 @@ export function createRecommendRunService({
|
|
|
1882
3065
|
post_action_clicked: 0,
|
|
1883
3066
|
image_capture_failed: 0,
|
|
1884
3067
|
detail_open_failed: 0,
|
|
1885
|
-
transient_recovered: 0,
|
|
1886
|
-
colleague_contact_checked: 0,
|
|
1887
|
-
recent_colleague_contact_skipped: 0,
|
|
1888
|
-
colleague_contact_panel_missing: 0,
|
|
1889
|
-
context_recoveries: 0,
|
|
1890
|
-
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
1891
|
-
current_city_only_effective: null,
|
|
1892
|
-
current_city_only_unavailable: false,
|
|
1893
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1894
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1895
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1896
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1897
|
-
human_rest_count: 0,
|
|
3068
|
+
transient_recovered: 0,
|
|
3069
|
+
colleague_contact_checked: 0,
|
|
3070
|
+
recent_colleague_contact_skipped: 0,
|
|
3071
|
+
colleague_contact_panel_missing: 0,
|
|
3072
|
+
context_recoveries: 0,
|
|
3073
|
+
current_city_only_requested: normalizedFilter.currentCityOnly,
|
|
3074
|
+
current_city_only_effective: null,
|
|
3075
|
+
current_city_only_unavailable: false,
|
|
3076
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
3077
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
3078
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
3079
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
3080
|
+
human_rest_count: 0,
|
|
1898
3081
|
human_rest_ms: 0,
|
|
1899
|
-
last_human_event: null
|
|
3082
|
+
last_human_event: null,
|
|
3083
|
+
debug_boundary_mode: debugBoundaryOptions.mode,
|
|
3084
|
+
debug_boundary_threshold: debugBoundaryOptions.threshold,
|
|
3085
|
+
debug_boundary_triggered: false,
|
|
3086
|
+
debug_boundary_trigger_count: 0
|
|
1900
3087
|
},
|
|
1901
3088
|
checkpoint: {},
|
|
1902
3089
|
task: (runControl) => workflow({
|
|
@@ -1939,7 +3126,11 @@ export function createRecommendRunService({
|
|
|
1939
3126
|
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1940
3127
|
humanBehavior: effectiveHumanBehavior,
|
|
1941
3128
|
skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
|
|
1942
|
-
colleagueContactWindowDays: normalizedColleagueContactWindowDays
|
|
3129
|
+
colleagueContactWindowDays: normalizedColleagueContactWindowDays,
|
|
3130
|
+
debugTestMode: debugBoundaryOptions.debugTestMode,
|
|
3131
|
+
debugForceListEndAfterProcessed: debugBoundaryOptions.debugForceListEndAfterProcessed,
|
|
3132
|
+
debugForceContextRecoveryAfterProcessed: debugBoundaryOptions.debugForceContextRecoveryAfterProcessed,
|
|
3133
|
+
debugForceCdpReconnectAfterProcessed: debugBoundaryOptions.debugForceCdpReconnectAfterProcessed
|
|
1943
3134
|
}, runControl)
|
|
1944
3135
|
});
|
|
1945
3136
|
}
|