@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
3
  import { createRunLifecycleManager } from "../../core/run/index.js";
4
4
  import {
5
5
  addTiming,
@@ -8,13 +8,13 @@ import {
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
10
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
- import {
12
- configureHumanInteraction,
13
- createHumanRestController,
14
- humanDelay,
15
- normalizeHumanBehaviorOptions,
16
- sleep
17
- } from "../../core/browser/index.js";
11
+ import {
12
+ configureHumanInteraction,
13
+ createHumanRestController,
14
+ humanDelay,
15
+ normalizeHumanBehaviorOptions,
16
+ sleep
17
+ } from "../../core/browser/index.js";
18
18
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
19
19
  import {
20
20
  compactCvAcquisitionState,
@@ -38,41 +38,43 @@ import {
38
38
  resolveInfiniteListFallbackPoint
39
39
  } from "../../core/infinite-list/index.js";
40
40
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
41
- import {
42
- callScreeningLlm,
43
- compactScreeningLlmResult,
44
- createFatalLlmRunError,
45
- createFailedLlmScreeningResult,
46
- isFatalLlmProviderError,
47
- llmResultToScreening,
48
- screenCandidate
49
- } from "../../core/screening/index.js";
50
- import {
51
- closeRecommendBlockingPanels,
52
- closeRecommendAvatarPreview,
53
- closeRecommendDetail,
54
- createRecommendDetailNetworkRecorder,
55
- extractRecommendDetailCandidate,
56
- isRecommendDetailOpenMissError,
57
- isStaleRecommendNodeError,
58
- openRecommendCardDetailWithFreshRetry,
59
- waitForRecommendDetailNetworkEvents
60
- } from "./detail.js";
61
41
  import {
62
- readRecommendCardCandidate,
63
- waitForRecommendCardNodeIds
64
- } from "./cards.js";
65
- import { selectAndConfirmFirstSafeFilter } from "./filters.js";
42
+ callScreeningLlm,
43
+ compactScreeningLlmResult,
44
+ createFatalLlmRunError,
45
+ createFailedLlmScreeningResult,
46
+ isFatalLlmProviderError,
47
+ llmResultToScreening,
48
+ screenCandidate
49
+ } from "../../core/screening/index.js";
66
50
  import {
67
- buildRecommendFilterSelectionOptions,
68
- refreshRecommendListAtEnd
69
- } from "./refresh.js";
70
- import { selectRecommendJob } from "./jobs.js";
51
+ closeRecommendBlockingPanels,
52
+ closeRecommendAvatarPreview,
53
+ closeRecommendDetail,
54
+ createRecommendDetailNetworkRecorder,
55
+ extractRecommendDetailCandidate,
56
+ isRecommendDetailOpenMissError,
57
+ isStaleRecommendNodeError,
58
+ openRecommendCardDetailWithFreshRetry,
59
+ waitForRecommendDetailNetworkEvents
60
+ } 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";
71
67
  import {
72
- normalizeRecommendPageScope,
73
- selectRecommendPageScope
74
- } from "./scopes.js";
75
- import { inspectRecentColleagueContact } from "./colleague-contact.js";
68
+ applyRecommendFilterEnvelopeStages,
69
+ buildRecommendFilterSelectionOptions,
70
+ refreshRecommendListAtEnd
71
+ } from "./refresh.js";
72
+ import { selectRecommendJob } from "./jobs.js";
73
+ import {
74
+ normalizeRecommendPageScope,
75
+ selectRecommendPageScope
76
+ } from "./scopes.js";
77
+ import { inspectRecentColleagueContact } from "./colleague-contact.js";
76
78
  import {
77
79
  RECOMMEND_BOTTOM_MARKER_SELECTORS,
78
80
  RECOMMEND_CARD_SELECTOR,
@@ -86,9 +88,304 @@ import {
86
88
  resolveRecommendPostAction,
87
89
  waitForRecommendDetailActionControls
88
90
  } from "./actions.js";
89
- import { getRecommendRoots } from "./roots.js";
90
-
91
- function normalizeLabels(labels = []) {
91
+ import { getRecommendRoots } from "./roots.js";
92
+
93
+ const RECOMMEND_DEBUG_BOUNDARY_MODES = Object.freeze({
94
+ list_end: "debug_force_list_end_after_processed",
95
+ context_recovery: "debug_force_context_recovery_after_processed",
96
+ cdp_reconnect: "debug_force_cdp_reconnect_after_processed"
97
+ });
98
+
99
+ function hasOwn(source, key) {
100
+ return Boolean(source && Object.prototype.hasOwnProperty.call(source, key));
101
+ }
102
+
103
+ function readDebugBoundaryValue(source, snakeKey, camelKey) {
104
+ if (hasOwn(source, snakeKey)) return source[snakeKey];
105
+ if (hasOwn(source, camelKey)) return source[camelKey];
106
+ return null;
107
+ }
108
+
109
+ function normalizeDebugBoundaryThreshold(raw, field) {
110
+ if (raw === undefined || raw === null || raw === "") return null;
111
+ const parsed = Number(raw);
112
+ if (!Number.isInteger(parsed) || parsed <= 0) {
113
+ const error = new Error(`${field} must be a positive integer`);
114
+ error.code = "INVALID_RECOMMEND_DEBUG_BOUNDARY";
115
+ throw error;
116
+ }
117
+ return parsed;
118
+ }
119
+
120
+ export function normalizeRecommendDebugBoundaryOptions(source = {}) {
121
+ const debugTestMode = source.debugTestMode === true || source.debug_test_mode === true;
122
+ const thresholds = {
123
+ list_end: normalizeDebugBoundaryThreshold(
124
+ readDebugBoundaryValue(
125
+ source,
126
+ RECOMMEND_DEBUG_BOUNDARY_MODES.list_end,
127
+ "debugForceListEndAfterProcessed"
128
+ ),
129
+ RECOMMEND_DEBUG_BOUNDARY_MODES.list_end
130
+ ),
131
+ context_recovery: normalizeDebugBoundaryThreshold(
132
+ readDebugBoundaryValue(
133
+ source,
134
+ RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery,
135
+ "debugForceContextRecoveryAfterProcessed"
136
+ ),
137
+ RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery
138
+ ),
139
+ cdp_reconnect: normalizeDebugBoundaryThreshold(
140
+ readDebugBoundaryValue(
141
+ source,
142
+ RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect,
143
+ "debugForceCdpReconnectAfterProcessed"
144
+ ),
145
+ RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect
146
+ )
147
+ };
148
+ const configured = Object.entries(thresholds).filter(([, threshold]) => threshold !== null);
149
+ if (configured.length > 1) {
150
+ const error = new Error(
151
+ `${configured.map(([mode]) => RECOMMEND_DEBUG_BOUNDARY_MODES[mode]).join(", ")} are mutually exclusive`
152
+ );
153
+ error.code = "RECOMMEND_DEBUG_BOUNDARIES_MUTUALLY_EXCLUSIVE";
154
+ throw error;
155
+ }
156
+ if (configured.length && !debugTestMode) {
157
+ const error = new Error(
158
+ `${RECOMMEND_DEBUG_BOUNDARY_MODES[configured[0][0]]} requires debug_test_mode=true`
159
+ );
160
+ error.code = "DEBUG_TEST_MODE_REQUIRED";
161
+ throw error;
162
+ }
163
+ const [configuredEntry = null] = configured;
164
+ return {
165
+ debugTestMode,
166
+ mode: configuredEntry?.[0] || null,
167
+ field: configuredEntry ? RECOMMEND_DEBUG_BOUNDARY_MODES[configuredEntry[0]] : null,
168
+ threshold: configuredEntry?.[1] ?? null,
169
+ debugForceListEndAfterProcessed: thresholds.list_end,
170
+ debugForceContextRecoveryAfterProcessed: thresholds.context_recovery,
171
+ debugForceCdpReconnectAfterProcessed: thresholds.cdp_reconnect
172
+ };
173
+ }
174
+
175
+ export function createRecommendDebugBoundaryController(source = {}) {
176
+ const config = normalizeRecommendDebugBoundaryOptions(source);
177
+ let triggered = false;
178
+ let triggerCount = 0;
179
+ return {
180
+ config,
181
+ take(processedCount) {
182
+ const processed = Math.max(0, Number(processedCount) || 0);
183
+ if (triggered || !config.mode || processed < config.threshold) return null;
184
+ triggered = true;
185
+ triggerCount += 1;
186
+ return {
187
+ mode: config.mode,
188
+ field: config.field,
189
+ threshold: config.threshold,
190
+ processed,
191
+ trigger_count: triggerCount
192
+ };
193
+ },
194
+ getState() {
195
+ return {
196
+ ...config,
197
+ triggered,
198
+ trigger_count: triggerCount
199
+ };
200
+ }
201
+ };
202
+ }
203
+
204
+ function compactListReadStaleDiagnostic(error, {
205
+ attempt = 0,
206
+ exhausted = false
207
+ } = {}) {
208
+ return {
209
+ code: error?.code || "RECOMMEND_LIST_READ_STALE_NODE",
210
+ message: String(error?.message || error || "Stale recommend list node").slice(0, 500),
211
+ phase: "recommend:list-read",
212
+ cdp_method: error?.cdp_method || null,
213
+ cdp_at: error?.cdp_at ? String(error.cdp_at).slice(0, 100) : null,
214
+ cdp_node_id: Number.isInteger(error?.cdp_node_id) ? error.cdp_node_id : null,
215
+ cdp_backend_node_id: Number.isInteger(error?.cdp_backend_node_id)
216
+ ? error.cdp_backend_node_id
217
+ : null,
218
+ cdp_param_keys: Array.isArray(error?.cdp_param_keys)
219
+ ? error.cdp_param_keys
220
+ .map((key) => String(key || "").trim())
221
+ .filter((key) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key))
222
+ .slice(0, 20)
223
+ : [],
224
+ attempt,
225
+ exhausted: Boolean(exhausted),
226
+ at: new Date().toISOString()
227
+ };
228
+ }
229
+
230
+ function annotateListReadStaleFailure(error, diagnostics, {
231
+ exhausted = false,
232
+ recoveryFailed = false
233
+ } = {}) {
234
+ if (!error || typeof error !== "object") return error;
235
+ error.phase = error.phase || "recommend:list-read";
236
+ error.list_read_stale_recovery_attempts = diagnostics;
237
+ if (exhausted) error.list_read_stale_recovery_exhausted = true;
238
+ if (recoveryFailed) error.list_read_stale_recovery_failed = true;
239
+ return error;
240
+ }
241
+
242
+ export async function acquireRecommendListReadWithStaleRecovery({
243
+ acquire,
244
+ recover,
245
+ maxRetries = 2,
246
+ onStale = null,
247
+ onRecoveryApplied = null,
248
+ onRecovered = null,
249
+ onExhausted = null
250
+ } = {}) {
251
+ if (typeof acquire !== "function") {
252
+ throw new Error("acquireRecommendListReadWithStaleRecovery requires acquire");
253
+ }
254
+ if (typeof recover !== "function") {
255
+ throw new Error("acquireRecommendListReadWithStaleRecovery requires recover");
256
+ }
257
+ const retryLimit = Math.max(0, Number.isInteger(maxRetries) ? maxRetries : 2);
258
+ const diagnostics = [];
259
+ let acquireAttempt = 0;
260
+ let pendingRecoveryDiagnostic = null;
261
+ while (true) {
262
+ acquireAttempt += 1;
263
+ try {
264
+ const result = await acquire({
265
+ acquireAttempt,
266
+ recoveryCount: diagnostics.filter((item) => item.recovered === true).length
267
+ });
268
+ if (pendingRecoveryDiagnostic) {
269
+ pendingRecoveryDiagnostic.recovered = true;
270
+ pendingRecoveryDiagnostic.recovered_at = new Date().toISOString();
271
+ if (typeof onRecovered === "function") {
272
+ await onRecovered({
273
+ diagnostic: pendingRecoveryDiagnostic,
274
+ diagnostics: diagnostics.slice()
275
+ });
276
+ }
277
+ pendingRecoveryDiagnostic = null;
278
+ }
279
+ return {
280
+ result,
281
+ acquire_attempts: acquireAttempt,
282
+ stale_diagnostics: diagnostics
283
+ };
284
+ } catch (error) {
285
+ if (!isStaleRecommendNodeError(error)) throw error;
286
+ pendingRecoveryDiagnostic = null;
287
+ const staleAttempt = diagnostics.length + 1;
288
+ const exhausted = staleAttempt > retryLimit;
289
+ const diagnostic = compactListReadStaleDiagnostic(error, {
290
+ attempt: staleAttempt,
291
+ exhausted
292
+ });
293
+ diagnostics.push(diagnostic);
294
+ if (exhausted) {
295
+ if (typeof onExhausted === "function") {
296
+ await onExhausted({ error, diagnostic, diagnostics: diagnostics.slice() });
297
+ }
298
+ throw annotateListReadStaleFailure(error, diagnostics, { exhausted: true });
299
+ }
300
+ if (typeof onStale === "function") {
301
+ await onStale({ error, diagnostic, diagnostics: diagnostics.slice() });
302
+ }
303
+ let recoveryResult = null;
304
+ try {
305
+ recoveryResult = await recover({ error, diagnostic, diagnostics: diagnostics.slice() });
306
+ } catch (recoveryError) {
307
+ if (recoveryError && typeof recoveryError === "object" && !recoveryError.cause) {
308
+ recoveryError.cause = error;
309
+ }
310
+ throw annotateListReadStaleFailure(recoveryError, diagnostics, {
311
+ recoveryFailed: true
312
+ });
313
+ }
314
+ diagnostic.recovery_applied = true;
315
+ diagnostic.recovery_mode = recoveryResult?.recovery_mode || "unknown";
316
+ diagnostic.recovery_applied_at = new Date().toISOString();
317
+ pendingRecoveryDiagnostic = diagnostic;
318
+ if (typeof onRecoveryApplied === "function") {
319
+ await onRecoveryApplied({
320
+ error,
321
+ diagnostic,
322
+ recoveryResult,
323
+ diagnostics: diagnostics.slice()
324
+ });
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ export async function recoverRecommendListReadStaleContext({
331
+ staleAttempt = 1,
332
+ listState,
333
+ rootReacquire,
334
+ contextReapply
335
+ } = {}) {
336
+ if (typeof rootReacquire !== "function") {
337
+ throw new Error("recoverRecommendListReadStaleContext requires rootReacquire");
338
+ }
339
+ if (typeof contextReapply !== "function") {
340
+ throw new Error("recoverRecommendListReadStaleContext requires contextReapply");
341
+ }
342
+ const processedKeys = new Set(listState?.processed_keys || []);
343
+ try {
344
+ if (Number(staleAttempt) <= 1) {
345
+ try {
346
+ const rootResult = await rootReacquire();
347
+ if (Number(rootResult?.card_count) <= 0) {
348
+ const noCardsError = new Error("Recommend list root reacquire returned no candidate cards");
349
+ noCardsError.code = "RECOMMEND_LIST_ROOT_REACQUIRE_NO_CARDS";
350
+ throw noCardsError;
351
+ }
352
+ resetInfiniteListForRefreshRound(listState, {
353
+ reason: "list_read_stale_node_root_reacquire",
354
+ round: 1,
355
+ method: "root_reacquire",
356
+ metadata: {
357
+ card_count: Number(rootResult?.card_count) || 0,
358
+ processed_count: processedKeys.size
359
+ }
360
+ });
361
+ return {
362
+ recovery_mode: "root_reacquire",
363
+ root_reacquire: rootResult || null
364
+ };
365
+ } catch (rootReacquireError) {
366
+ const contextResult = await contextReapply({ rootReacquireError });
367
+ return {
368
+ recovery_mode: "context_reapply",
369
+ escalated_from: "root_reacquire",
370
+ root_reacquire_error: compactListReadStaleDiagnostic(rootReacquireError, {
371
+ attempt: 1
372
+ }),
373
+ context_reapply: contextResult || null
374
+ };
375
+ }
376
+ }
377
+ const contextResult = await contextReapply({ rootReacquireError: null });
378
+ return {
379
+ recovery_mode: "context_reapply",
380
+ escalated_from: "repeated_stale",
381
+ context_reapply: contextResult || null
382
+ };
383
+ } finally {
384
+ for (const key of processedKeys) listState?.processed_keys?.add(key);
385
+ }
386
+ }
387
+
388
+ function normalizeLabels(labels = []) {
92
389
  return labels.map((label) => String(label || "").trim()).filter(Boolean);
93
390
  }
94
391
 
@@ -104,26 +401,46 @@ function isRefreshableListStall(reason = "") {
104
401
  function normalizeFilter(filter = {}) {
105
402
  const filterGroups = Array.isArray(filter.filterGroups)
106
403
  ? filter.filterGroups
107
- : Array.isArray(filter.groups)
108
- ? filter.groups
109
- : [];
110
- return {
111
- enabled: filter.enabled !== false,
112
- group: String(filter.group || ""),
113
- labels: normalizeLabels(filter.labels || filter.filterLabels || []),
114
- selectAllLabels: Boolean(filter.selectAllLabels),
115
- filterGroups: filterGroups.map((group) => ({
116
- group: String(group?.group || ""),
117
- labels: normalizeLabels(group?.labels || group?.filterLabels || []),
118
- selectAllLabels: group?.selectAllLabels !== false
119
- })).filter((group) => group.group || group.labels.length)
120
- };
121
- }
122
-
123
- function compactFilterResult(filterResult) {
124
- if (!filterResult) return null;
125
- return {
126
- opened_panel: Boolean(filterResult.opened_panel),
404
+ : Array.isArray(filter.groups)
405
+ ? filter.groups
406
+ : [];
407
+ return {
408
+ enabled: filter.enabled !== false,
409
+ currentCityOnly: filter.currentCityOnly === true || filter.current_city_only === true,
410
+ group: String(filter.group || ""),
411
+ labels: normalizeLabels(filter.labels || filter.filterLabels || []),
412
+ selectAllLabels: Boolean(filter.selectAllLabels),
413
+ allowUnlimited: filter.allowUnlimited === true,
414
+ verifySticky: filter.verifySticky === true,
415
+ filterGroups: filterGroups.map((group) => ({
416
+ group: String(group?.group || ""),
417
+ labels: normalizeLabels(group?.labels || group?.filterLabels || []),
418
+ selectAllLabels: group?.selectAllLabels !== false,
419
+ allowUnlimited: group?.allowUnlimited === true,
420
+ verifySticky: group?.verifySticky === true
421
+ })).filter((group) => group.group || group.labels.length)
422
+ };
423
+ }
424
+
425
+ export function compactFilterResult(filterResult) {
426
+ if (!filterResult) return null;
427
+ return {
428
+ opened_panel: Boolean(filterResult.opened_panel),
429
+ requested_groups: (filterResult.requested_groups || []).map((group) => ({
430
+ group: group.group,
431
+ labels: group.labels || [],
432
+ select_all_labels: group.select_all_labels !== false,
433
+ allow_unlimited: Boolean(group.allow_unlimited),
434
+ verify_sticky: Boolean(group.verify_sticky)
435
+ })),
436
+ effective_groups: (filterResult.sticky_verification?.groups || []).map((group) => ({
437
+ group: group.group,
438
+ requested_labels: group.requested_labels || [],
439
+ active_labels: group.active_labels || [],
440
+ verified: Boolean(group.verified),
441
+ unavailable: Boolean(group.unavailable),
442
+ reason: group.reason || null
443
+ })),
127
444
  selected_option: filterResult.selected_option
128
445
  ? {
129
446
  group: filterResult.selected_option.group,
@@ -133,49 +450,98 @@ function compactFilterResult(filterResult) {
133
450
  }
134
451
  : null,
135
452
  selected_options: (filterResult.selected_options || []).map((option) => ({
136
- group: option.group,
137
- label: option.label,
138
- was_active: Boolean(option.was_active),
139
- clicked: option.clicked !== false
140
- })),
141
- confirmed: Boolean(filterResult.confirmed),
142
- before_counts: filterResult.before_counts,
143
- after_confirm_counts: filterResult.after_confirm_counts
144
- };
145
- }
146
-
147
- function compactJobSelection(jobSelection) {
148
- if (!jobSelection) return null;
149
- return {
150
- requested: jobSelection.requested || "",
151
- selected: Boolean(jobSelection.selected),
152
- already_current: Boolean(jobSelection.already_current),
153
- reason: jobSelection.reason || null,
154
- selected_option: jobSelection.selected_option || null,
155
- menu_close: jobSelection.menu_close
156
- ? {
157
- ok: Boolean(jobSelection.menu_close.ok),
158
- closed: Boolean(jobSelection.menu_close.closed),
159
- reason: jobSelection.menu_close.reason || ""
160
- }
161
- : null,
162
- sticky_verification: jobSelection.sticky_verification
453
+ group: option.group,
454
+ label: option.label,
455
+ was_active: Boolean(option.was_active),
456
+ clicked: option.clicked !== false
457
+ })),
458
+ unavailable: Boolean(filterResult.unavailable),
459
+ unavailable_groups: filterResult.unavailable_groups || [],
460
+ confirmed: Boolean(filterResult.confirmed),
461
+ sticky_verification: filterResult.sticky_verification || null,
462
+ attempts: {
463
+ initial_close: filterResult.initial_close_attempts || [],
464
+ open: (filterResult.open_attempts || []).map((attempt) => ({
465
+ selector: attempt.selector || null,
466
+ node_id: attempt.node_id || null,
467
+ click_target: attempt.click_target || null
468
+ })),
469
+ confirmation: (filterResult.confirm_attempts || []).map((attempt) => ({
470
+ node_id: attempt.node_id || null,
471
+ label: attempt.label || null,
472
+ clicked: Boolean(attempt.clicked),
473
+ errors: (attempt.errors || []).map((error) => ({
474
+ node_id: error.node_id || null,
475
+ message: error.message || String(error)
476
+ }))
477
+ }))
478
+ },
479
+ before_counts: filterResult.before_counts,
480
+ after_confirm_counts: filterResult.after_confirm_counts
481
+ };
482
+ }
483
+
484
+ function compactCurrentCityOnlyResult(result) {
485
+ if (!result) return null;
486
+ return {
487
+ requested: Boolean(result.requested),
488
+ effective: typeof result.effective === "boolean" ? result.effective : null,
489
+ available: result.available !== false,
490
+ unavailable: Boolean(result.unavailable),
491
+ reason: result.reason || null,
492
+ clicked: Boolean(result.clicked),
493
+ current_city_label: result.current_city_label || null,
494
+ before: result.before || null,
495
+ after_toggle: result.after_toggle || null,
496
+ confirmation: result.confirmation || null,
497
+ sticky_verification: result.sticky_verification
163
498
  ? {
164
- verified: Boolean(jobSelection.sticky_verification.verified),
165
- current_label: jobSelection.sticky_verification.current_label_without_salary
166
- || jobSelection.sticky_verification.current_label
167
- || "",
168
- visible_option_count: jobSelection.sticky_verification.visible_option_count || 0,
169
- menu_close: jobSelection.sticky_verification.menu_close
170
- ? {
171
- ok: Boolean(jobSelection.sticky_verification.menu_close.ok),
172
- closed: Boolean(jobSelection.sticky_verification.menu_close.closed),
173
- reason: jobSelection.sticky_verification.menu_close.reason || ""
174
- }
175
- : null
499
+ verified: Boolean(result.sticky_verification.verified),
500
+ expected: Boolean(result.sticky_verification.expected),
501
+ actual: typeof result.sticky_verification.actual === "boolean"
502
+ ? result.sticky_verification.actual
503
+ : null,
504
+ state_source: result.sticky_verification.state_source || null,
505
+ close_confirmation: result.sticky_verification.close_confirmation || null
176
506
  }
177
507
  : null,
178
- options: (jobSelection.options || []).map((option) => ({
508
+ attempts: Array.isArray(result.attempts) ? result.attempts : [],
509
+ evidence: result.evidence || null
510
+ };
511
+ }
512
+
513
+ function compactJobSelection(jobSelection) {
514
+ if (!jobSelection) return null;
515
+ return {
516
+ requested: jobSelection.requested || "",
517
+ selected: Boolean(jobSelection.selected),
518
+ already_current: Boolean(jobSelection.already_current),
519
+ reason: jobSelection.reason || null,
520
+ selected_option: jobSelection.selected_option || null,
521
+ menu_close: jobSelection.menu_close
522
+ ? {
523
+ ok: Boolean(jobSelection.menu_close.ok),
524
+ closed: Boolean(jobSelection.menu_close.closed),
525
+ reason: jobSelection.menu_close.reason || ""
526
+ }
527
+ : null,
528
+ sticky_verification: jobSelection.sticky_verification
529
+ ? {
530
+ verified: Boolean(jobSelection.sticky_verification.verified),
531
+ current_label: jobSelection.sticky_verification.current_label_without_salary
532
+ || jobSelection.sticky_verification.current_label
533
+ || "",
534
+ visible_option_count: jobSelection.sticky_verification.visible_option_count || 0,
535
+ menu_close: jobSelection.sticky_verification.menu_close
536
+ ? {
537
+ ok: Boolean(jobSelection.sticky_verification.menu_close.ok),
538
+ closed: Boolean(jobSelection.sticky_verification.menu_close.closed),
539
+ reason: jobSelection.sticky_verification.menu_close.reason || ""
540
+ }
541
+ : null
542
+ }
543
+ : null,
544
+ options: (jobSelection.options || []).map((option) => ({
179
545
  label: option.label,
180
546
  label_without_salary: option.label_without_salary,
181
547
  current: Boolean(option.current),
@@ -225,20 +591,20 @@ function compactCandidate(candidate) {
225
591
  };
226
592
  }
227
593
 
228
- function compactDetail(detailResult) {
229
- if (!detailResult) return null;
230
- return {
231
- popup_text_length: detailResult.detail?.popup_text?.length || 0,
232
- resume_text_length: detailResult.detail?.resume_text?.length || 0,
594
+ function compactDetail(detailResult) {
595
+ if (!detailResult) return null;
596
+ return {
597
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
598
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
233
599
  network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
234
600
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
235
- cv_acquisition: detailResult.cv_acquisition || null,
236
- colleague_contact: detailResult.colleague_contact || null,
237
- image_evidence: summarizeImageEvidence(detailResult.image_evidence),
238
- llm_screening: compactScreeningLlmResult(detailResult.llm_result),
239
- close_result: detailResult.close_result
240
- };
241
- }
601
+ cv_acquisition: detailResult.cv_acquisition || null,
602
+ colleague_contact: detailResult.colleague_contact || null,
603
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
604
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
605
+ close_result: detailResult.close_result
606
+ };
607
+ }
242
608
 
243
609
  function normalizeScreeningMode(value) {
244
610
  const normalized = String(value || "llm").trim().toLowerCase();
@@ -287,22 +653,22 @@ async function runRecommendPostAction({
287
653
  reason: ""
288
654
  };
289
655
 
290
- if (!screening?.passed) {
291
- result.reason = "screening_not_passed";
292
- return result;
293
- }
294
- if (plan.effective === "none") {
295
- result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
296
- return result;
297
- }
298
-
299
- const summary = actionDiscovery?.summary || {};
300
- const control = summary.greet;
301
- if (!control?.found) {
302
- result.reason = `${plan.effective}_control_not_found`;
303
- return result;
304
- }
305
- result.control = control;
656
+ if (!screening?.passed) {
657
+ result.reason = "screening_not_passed";
658
+ return result;
659
+ }
660
+ if (plan.effective === "none") {
661
+ result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
662
+ return result;
663
+ }
664
+
665
+ const summary = actionDiscovery?.summary || {};
666
+ const control = summary.greet;
667
+ if (!control?.found) {
668
+ result.reason = `${plan.effective}_control_not_found`;
669
+ return result;
670
+ }
671
+ result.control = control;
306
672
 
307
673
  if (plan.effective === "greet" && control.continue_chat) {
308
674
  result.reason = "already_connected_continue_chat";
@@ -319,10 +685,10 @@ async function runRecommendPostAction({
319
685
  result.reason = "greet_control_not_available";
320
686
  return result;
321
687
  }
322
- if (control.disabled) {
323
- result.reason = `${plan.effective}_control_disabled`;
324
- return result;
325
- }
688
+ if (control.disabled) {
689
+ result.reason = `${plan.effective}_control_disabled`;
690
+ return result;
691
+ }
326
692
  if (!executePostAction) {
327
693
  result.reason = "dry_run_post_action";
328
694
  result.would_click = true;
@@ -357,18 +723,18 @@ async function runRecommendPostAction({
357
723
  timeoutMs: 2500,
358
724
  intervalMs: 300,
359
725
  requireAny: false
360
- });
361
- const afterSummary = afterDiscovery?.summary || {};
362
- const afterControl = afterSummary.greet;
363
- result.action_discovery_after = compactActionDiscovery(afterDiscovery);
364
- result.control_after = afterControl || null;
365
- if (plan.effective === "greet") {
366
- result.verified_after_click = Boolean(
367
- afterControl?.continue_chat
368
- || String(afterControl?.label || "").includes("继续沟通")
369
- );
370
- }
371
- } catch (error) {
726
+ });
727
+ const afterSummary = afterDiscovery?.summary || {};
728
+ const afterControl = afterSummary.greet;
729
+ result.action_discovery_after = compactActionDiscovery(afterDiscovery);
730
+ result.control_after = afterControl || null;
731
+ if (plan.effective === "greet") {
732
+ result.verified_after_click = Boolean(
733
+ afterControl?.continue_chat
734
+ || String(afterControl?.label || "").includes("继续沟通")
735
+ );
736
+ }
737
+ } catch (error) {
372
738
  result.verify_error = {
373
739
  message: error?.message || String(error)
374
740
  };
@@ -380,178 +746,244 @@ function compactRefreshAttempt(refreshAttempt) {
380
746
  if (!refreshAttempt) return null;
381
747
  return {
382
748
  ok: Boolean(refreshAttempt.ok),
383
- method: refreshAttempt.method || "",
384
- reason: refreshAttempt.reason || null,
385
- error: refreshAttempt.error || null,
386
- forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
387
- target_url: refreshAttempt.target_url || null,
388
- card_count: refreshAttempt.card_count || 0,
389
- elapsed_ms: refreshAttempt.elapsed_ms || 0,
390
- recovery_settle: refreshAttempt.recovery_settle
391
- ? {
392
- ok: Boolean(refreshAttempt.recovery_settle.ok),
393
- status: refreshAttempt.recovery_settle.status || "",
394
- reason: refreshAttempt.recovery_settle.reason || "",
395
- elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
396
- }
397
- : null,
398
- attempts: (refreshAttempt.attempts || []).map((attempt) => ({
749
+ method: refreshAttempt.method || "",
750
+ reason: refreshAttempt.reason || null,
751
+ error: refreshAttempt.error || null,
752
+ error_diagnostic: refreshAttempt.error_diagnostic || null,
753
+ forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
754
+ target_url: refreshAttempt.target_url || null,
755
+ card_count: refreshAttempt.card_count || 0,
756
+ elapsed_ms: refreshAttempt.elapsed_ms || 0,
757
+ recovery_settle: refreshAttempt.recovery_settle
758
+ ? {
759
+ ok: Boolean(refreshAttempt.recovery_settle.ok),
760
+ status: refreshAttempt.recovery_settle.status || "",
761
+ reason: refreshAttempt.recovery_settle.reason || "",
762
+ elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
763
+ }
764
+ : null,
765
+ attempts: (refreshAttempt.attempts || []).map((attempt) => ({
399
766
  ok: Boolean(attempt.ok),
400
- method: attempt.method || "",
401
- reason: attempt.reason || null,
402
- error: attempt.error || null,
403
- label: attempt.label || null,
767
+ method: attempt.method || "",
768
+ reason: attempt.reason || null,
769
+ error: attempt.error || null,
770
+ error_diagnostic: attempt.error_diagnostic || null,
771
+ label: attempt.label || null,
404
772
  before_card_count: attempt.before_card_count || 0,
405
773
  after_card_count: attempt.after_card_count || 0,
406
774
  card_count: attempt.card_count || 0,
407
- elapsed_ms: attempt.elapsed_ms || 0
775
+ elapsed_ms: attempt.elapsed_ms || 0,
776
+ current_city_only: compactCurrentCityOnlyResult(attempt.current_city_only),
777
+ current_city_only_attempts: (attempt.current_city_only_attempts || []).map((cityAttempt) => ({
778
+ ok: Boolean(cityAttempt.ok),
779
+ method: cityAttempt.method || "current_city_only_reapply",
780
+ reason: cityAttempt.reason || null,
781
+ error: cityAttempt.error || null,
782
+ error_diagnostic: cityAttempt.error_diagnostic || null,
783
+ attempt: cityAttempt.attempt || 0,
784
+ result: compactCurrentCityOnlyResult(cityAttempt.result)
785
+ })),
786
+ filter: compactFilterResult(attempt.filter)
408
787
  })),
409
- filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
788
+ current_city_only_attempts: (refreshAttempt.current_city_only_attempts || []).map((attempt) => ({
410
789
  ok: Boolean(attempt.ok),
411
- method: attempt.method || "filter_reapply",
790
+ method: attempt.method || "current_city_only_reapply",
412
791
  reason: attempt.reason || null,
413
792
  error: attempt.error || null,
414
- attempt: attempt.attempt || 0
793
+ error_diagnostic: attempt.error_diagnostic || null,
794
+ attempt: attempt.attempt || 0,
795
+ result: compactCurrentCityOnlyResult(attempt.result)
415
796
  })),
416
- job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
797
+ filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
417
798
  ok: Boolean(attempt.ok),
799
+ method: attempt.method || "filter_reapply",
800
+ reason: attempt.reason || null,
801
+ error: attempt.error || null,
802
+ error_diagnostic: attempt.error_diagnostic || null,
803
+ attempt: attempt.attempt || 0
804
+ })),
805
+ job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
806
+ ok: Boolean(attempt.ok),
418
807
  method: attempt.method || "job_select",
419
808
  reason: attempt.reason || null,
420
809
  error: attempt.error || null,
421
- attempt: attempt.attempt || 0,
422
- iframe_document_node_id: attempt.iframe_document_node_id || 0,
810
+ error_diagnostic: attempt.error_diagnostic || null,
811
+ attempt: attempt.attempt || 0,
812
+ iframe_document_node_id: attempt.iframe_document_node_id || 0,
423
813
  selected: Boolean(attempt.selected),
424
814
  selection_reason: attempt.selection_reason || null
425
815
  })),
426
816
  job_selection: compactJobSelection(refreshAttempt.job_selection),
427
817
  page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
818
+ current_city_only: compactCurrentCityOnlyResult(refreshAttempt.current_city_only),
428
819
  filter: compactFilterResult(refreshAttempt.filter)
429
820
  };
430
821
  }
431
-
822
+
432
823
  export function countRecommendResultStatuses(results = [], {
433
- greetCount = 0
434
- } = {}) {
435
- return {
436
- processed: results.length,
437
- screened: results.length,
438
- detail_opened: results.filter((item) => item.detail).length,
439
- passed: results.filter((item) => item.screening?.passed).length,
440
- skipped: results.filter((item) => item.screening?.passed === false).length,
441
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
442
- greet_count: greetCount,
443
- post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
444
- image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
824
+ greetCount = 0
825
+ } = {}) {
826
+ return {
827
+ processed: results.length,
828
+ screened: results.length,
829
+ detail_opened: results.filter((item) => item.detail).length,
830
+ passed: results.filter((item) => item.screening?.passed).length,
831
+ skipped: results.filter((item) => item.screening?.passed === false).length,
832
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
833
+ greet_count: greetCount,
834
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
835
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
445
836
  detail_open_failed: results.filter((item) => (
446
837
  item.error?.code === "DETAIL_STALE_NODE"
447
838
  || item.error?.code === "DETAIL_OPEN_FAILED"
448
- )).length,
449
- transient_recovered: results.filter((item) => (
450
- item.error?.code === "DETAIL_STALE_NODE"
451
- || item.error?.code === "DETAIL_OPEN_FAILED"
452
- || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
453
- || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
454
- || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
455
- )).length,
456
- colleague_contact_checked: results.filter((item) => item.detail?.colleague_contact?.checked).length,
457
- recent_colleague_contact_skipped: results.filter((item) => (
458
- item.screening?.status === "skip"
459
- && item.screening?.reasons?.includes("skipped_recent_colleague_contact")
460
- )).length,
461
- colleague_contact_panel_missing: results.filter((item) => (
462
- item.detail?.colleague_contact?.reason === "panel_missing"
463
- )).length
464
- };
465
- }
839
+ )).length,
840
+ transient_recovered: results.filter((item) => (
841
+ item.error?.code === "DETAIL_STALE_NODE"
842
+ || item.error?.code === "DETAIL_OPEN_FAILED"
843
+ || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
844
+ || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
845
+ || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
846
+ )).length,
847
+ colleague_contact_checked: results.filter((item) => item.detail?.colleague_contact?.checked).length,
848
+ recent_colleague_contact_skipped: results.filter((item) => (
849
+ item.screening?.status === "skip"
850
+ && item.screening?.reasons?.includes("skipped_recent_colleague_contact")
851
+ )).length,
852
+ colleague_contact_panel_missing: results.filter((item) => (
853
+ item.detail?.colleague_contact?.reason === "panel_missing"
854
+ )).length
855
+ };
856
+ }
466
857
 
467
- function countPassedResults(results = []) {
468
- return countRecommendResultStatuses(results).passed;
469
- }
470
-
471
- function compactCloseResult(closeResult) {
472
- if (!closeResult) return null;
473
- const result = {
474
- closed: Boolean(closeResult.closed),
475
- reason: closeResult.reason || null,
476
- probe: closeResult.probe || null,
477
- attempts: closeResult.attempts || [],
478
- verification: closeResult.verification || null
479
- };
480
- if (closeResult.already_closed !== undefined) {
481
- result.already_closed = Boolean(closeResult.already_closed);
482
- }
483
- return result;
484
- }
485
-
486
- function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
487
- if (!error) return null;
488
- const result = {
489
- code: error.code || fallbackCode,
490
- message: error.message || String(error)
491
- };
492
- if (error.close_result) {
493
- result.close_result = compactCloseResult(error.close_result);
494
- }
495
- if (error.phase) {
496
- result.phase = error.phase;
497
- }
858
+ function countPassedResults(results = []) {
859
+ return countRecommendResultStatuses(results).passed;
860
+ }
861
+
862
+ function compactCloseResult(closeResult) {
863
+ if (!closeResult) return null;
864
+ const result = {
865
+ closed: Boolean(closeResult.closed),
866
+ reason: closeResult.reason || null,
867
+ probe: closeResult.probe || null,
868
+ attempts: closeResult.attempts || [],
869
+ verification: closeResult.verification || null
870
+ };
871
+ if (closeResult.already_closed !== undefined) {
872
+ result.already_closed = Boolean(closeResult.already_closed);
873
+ }
874
+ return result;
875
+ }
876
+
877
+ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
878
+ if (!error) return null;
879
+ const result = {
880
+ code: error.code || fallbackCode,
881
+ message: error.message || String(error)
882
+ };
883
+ if (error.close_result) {
884
+ result.close_result = compactCloseResult(error.close_result);
885
+ }
886
+ if (error.phase) {
887
+ result.phase = error.phase;
888
+ }
498
889
  if (error.refresh_attempt) {
499
890
  result.refresh_attempt = error.refresh_attempt;
500
891
  }
501
- if (error.list_end_reason) {
502
- result.list_end_reason = error.list_end_reason;
503
- }
504
- if (error.target_count != null) {
505
- result.target_count = error.target_count;
506
- }
507
- if (error.passed_count != null) {
508
- result.passed_count = error.passed_count;
509
- }
510
- if (Array.isArray(error.recommend_detail_open_attempts)) {
511
- result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
512
- }
513
- if (Array.isArray(error.click_attempts)) {
514
- result.click_attempts = error.click_attempts;
892
+ if (error.error_diagnostic) {
893
+ result.error_diagnostic = error.error_diagnostic;
515
894
  }
516
- if (error.avatar_preview) {
517
- result.avatar_preview = {
518
- open: Boolean(error.avatar_preview.open),
519
- selector: error.avatar_preview.preview?.selector || null,
520
- rect: error.avatar_preview.preview?.rect || null
521
- };
522
- }
523
- return result;
524
- }
895
+ if (error.list_end_reason) {
896
+ result.list_end_reason = error.list_end_reason;
897
+ }
898
+ if (error.target_count != null) {
899
+ result.target_count = error.target_count;
900
+ }
901
+ if (error.passed_count != null) {
902
+ result.passed_count = error.passed_count;
903
+ }
904
+ if (Array.isArray(error.recommend_detail_open_attempts)) {
905
+ result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
906
+ }
907
+ if (Array.isArray(error.click_attempts)) {
908
+ result.click_attempts = error.click_attempts;
909
+ }
910
+ if (error.avatar_preview) {
911
+ result.avatar_preview = {
912
+ open: Boolean(error.avatar_preview.open),
913
+ selector: error.avatar_preview.preview?.selector || null,
914
+ rect: error.avatar_preview.preview?.rect || null
915
+ };
916
+ }
917
+ return result;
918
+ }
525
919
 
526
- function createRecommendCloseFailureError(closeResult) {
527
- const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
528
- error.code = "DETAIL_CLOSE_FAILED";
529
- error.close_result = closeResult || null;
530
- return error;
920
+ function createRecommendCloseFailureError(closeResult) {
921
+ const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
922
+ error.code = "DETAIL_CLOSE_FAILED";
923
+ error.close_result = closeResult || null;
924
+ return error;
925
+ }
926
+
927
+ function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "") {
928
+ const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
929
+ error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
930
+ error.close_result = closeResult || null;
931
+ error.phase = phase || null;
932
+ return error;
933
+ }
934
+
935
+ function findRecommendRefreshErrorDiagnostic(refreshAttempt) {
936
+ if (!refreshAttempt || typeof refreshAttempt !== "object") return null;
937
+ if (refreshAttempt.error_diagnostic) return refreshAttempt.error_diagnostic;
938
+ const candidates = [
939
+ ...(refreshAttempt.attempts || []),
940
+ ...(refreshAttempt.current_city_only_attempts || []),
941
+ ...(refreshAttempt.filter_reapply_attempts || []),
942
+ ...(refreshAttempt.job_selection_attempts || [])
943
+ ].reverse();
944
+ for (const attempt of candidates) {
945
+ if (attempt?.error_diagnostic) return attempt.error_diagnostic;
946
+ const nestedCityAttempts = (attempt?.current_city_only_attempts || []).slice().reverse();
947
+ for (const cityAttempt of nestedCityAttempts) {
948
+ if (cityAttempt?.error_diagnostic) return cityAttempt.error_diagnostic;
949
+ }
950
+ }
951
+ return null;
531
952
  }
532
953
 
533
- function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "") {
534
- const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
535
- error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
536
- error.close_result = closeResult || null;
537
- error.phase = phase || null;
954
+ function attachRecommendRefreshErrorDiagnostic(error, refreshAttempt) {
955
+ const diagnostic = findRecommendRefreshErrorDiagnostic(refreshAttempt);
956
+ if (!error || !diagnostic) return error;
957
+ error.error_diagnostic = diagnostic;
958
+ for (const key of [
959
+ "cdp_method",
960
+ "cdp_at",
961
+ "cdp_node_id",
962
+ "cdp_backend_node_id",
963
+ "cdp_search_id",
964
+ "cdp_param_keys"
965
+ ]) {
966
+ if (diagnostic[key] !== undefined && error[key] === undefined) {
967
+ error[key] = diagnostic[key];
968
+ }
969
+ }
538
970
  return error;
539
971
  }
540
972
 
541
- function createRecommendRefreshFailureError(refreshAttempt, {
542
- listEndReason = "",
543
- targetCount = 0,
544
- passedCount = 0
545
- } = {}) {
546
- const reason = refreshAttempt?.reason || "refresh_failed";
547
- const detail = refreshAttempt?.error ? `: ${refreshAttempt.error}` : "";
548
- const error = new Error(`Recommend refresh failed before target was reached (${reason}${detail})`);
549
- error.code = "RECOMMEND_END_REFRESH_FAILED";
550
- error.refresh_attempt = refreshAttempt || null;
551
- error.list_end_reason = listEndReason || null;
973
+ export function createRecommendRefreshFailureError(refreshAttempt, {
974
+ listEndReason = "",
975
+ targetCount = 0,
976
+ passedCount = 0
977
+ } = {}) {
978
+ const reason = refreshAttempt?.reason || "refresh_failed";
979
+ const detail = refreshAttempt?.error ? `: ${refreshAttempt.error}` : "";
980
+ const error = new Error(`Recommend refresh failed before target was reached (${reason}${detail})`);
981
+ error.code = "RECOMMEND_END_REFRESH_FAILED";
982
+ error.refresh_attempt = refreshAttempt || null;
983
+ error.list_end_reason = listEndReason || null;
552
984
  error.target_count = targetCount;
553
985
  error.passed_count = passedCount;
554
- return error;
986
+ return attachRecommendRefreshErrorDiagnostic(error, refreshAttempt);
555
987
  }
556
988
 
557
989
  export function isRecoverableImageCaptureError(error) {
@@ -615,41 +1047,41 @@ function createImageCaptureFailureScreening(candidate, error) {
615
1047
  };
616
1048
  }
617
1049
 
618
- export function isRecoverableRecommendDetailError(error) {
619
- return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
620
- }
1050
+ export function isRecoverableRecommendDetailError(error) {
1051
+ return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
1052
+ }
621
1053
 
622
1054
  function compactRecoverableDetailError(error) {
623
1055
  return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
624
1056
  }
625
1057
 
626
- function createRecoverableDetailFailureScreening(candidate, error) {
627
- return {
628
- status: "fail",
629
- passed: false,
630
- score: 0,
631
- reasons: isStaleRecommendNodeError(error)
632
- ? ["detail_open_failed", "stale_node"]
633
- : isRecommendDetailOpenMissError(error)
634
- ? ["detail_open_failed", "detail_open_miss"]
635
- : ["detail_open_failed"],
1058
+ function createRecoverableDetailFailureScreening(candidate, error) {
1059
+ return {
1060
+ status: "fail",
1061
+ passed: false,
1062
+ score: 0,
1063
+ reasons: isStaleRecommendNodeError(error)
1064
+ ? ["detail_open_failed", "stale_node"]
1065
+ : isRecommendDetailOpenMissError(error)
1066
+ ? ["detail_open_failed", "detail_open_miss"]
1067
+ : ["detail_open_failed"],
636
1068
  error: compactRecoverableDetailError(error),
637
- candidate
638
- };
639
- }
640
-
641
- function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
642
- const matched = colleagueContact?.matched_row || null;
643
- return {
644
- status: "skip",
645
- passed: false,
646
- score: 0,
647
- reasons: ["skipped_recent_colleague_contact"],
648
- reason: matched?.text || "Candidate has recent colleague contact history",
649
- matched_colleague_contact: matched,
650
- candidate
651
- };
652
- }
1069
+ candidate
1070
+ };
1071
+ }
1072
+
1073
+ function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
1074
+ const matched = colleagueContact?.matched_row || null;
1075
+ return {
1076
+ status: "skip",
1077
+ passed: false,
1078
+ score: 0,
1079
+ reasons: ["skipped_recent_colleague_contact"],
1080
+ reason: matched?.text || "Candidate has recent colleague contact history",
1081
+ matched_colleague_contact: matched,
1082
+ candidate
1083
+ };
1084
+ }
653
1085
 
654
1086
  export async function runRecommendWorkflow({
655
1087
  client,
@@ -681,47 +1113,57 @@ export async function runRecommendWorkflow({
681
1113
  executePostAction = true,
682
1114
  actionTimeoutMs = 8000,
683
1115
  actionIntervalMs = 500,
684
- actionAfterClickDelayMs = 900,
685
- screeningMode = "llm",
686
- llmConfig = null,
687
- llmTimeoutMs = 120000,
688
- llmImageLimit = 8,
689
- llmImageDetail = "high",
690
- imageOutputDir = "",
1116
+ actionAfterClickDelayMs = 900,
1117
+ screeningMode = "llm",
1118
+ llmConfig = null,
1119
+ llmTimeoutMs = 120000,
1120
+ llmImageLimit = 8,
1121
+ llmImageDetail = "high",
1122
+ imageOutputDir = "",
691
1123
  humanRestEnabled = false,
692
1124
  humanBehavior = null,
693
1125
  skipRecentColleagueContacted = true,
694
- colleagueContactWindowDays = 14
1126
+ colleagueContactWindowDays = 14,
1127
+ debugTestMode = false,
1128
+ debugForceListEndAfterProcessed = null,
1129
+ debugForceContextRecoveryAfterProcessed = null,
1130
+ debugForceCdpReconnectAfterProcessed = null
695
1131
  } = {}, runControl) {
696
1132
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
697
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
698
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
699
- });
700
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
701
- configureHumanInteraction(client, {
702
- enabled: effectiveHumanBehavior.enabled,
703
- clickMovementEnabled: effectiveHumanBehavior.clickMovement,
704
- textEntryEnabled: effectiveHumanBehavior.textEntry,
705
- safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
706
- actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
1133
+ const debugBoundary = createRecommendDebugBoundaryController({
1134
+ debugTestMode,
1135
+ debugForceListEndAfterProcessed,
1136
+ debugForceContextRecoveryAfterProcessed,
1137
+ debugForceCdpReconnectAfterProcessed
707
1138
  });
708
- const humanRestController = createHumanRestController({
709
- enabled: effectiveHumanRestEnabled,
710
- shortRestEnabled: effectiveHumanBehavior.shortRest,
711
- batchRestEnabled: effectiveHumanBehavior.batchRest,
712
- restLevel: effectiveHumanBehavior.restLevel
713
- });
714
- const normalizedFilter = normalizeFilter(filter);
1139
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1140
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1141
+ });
1142
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1143
+ configureHumanInteraction(client, {
1144
+ enabled: effectiveHumanBehavior.enabled,
1145
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
1146
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
1147
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
1148
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
1149
+ });
1150
+ const humanRestController = createHumanRestController({
1151
+ enabled: effectiveHumanRestEnabled,
1152
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
1153
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
1154
+ restLevel: effectiveHumanBehavior.restLevel
1155
+ });
1156
+ const normalizedFilter = normalizeFilter(filter);
715
1157
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
716
1158
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
717
1159
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
718
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
719
- const useLlmScreening = normalizedScreeningMode !== "deterministic";
720
- const postActionEnabled = normalizedPostAction !== "none";
721
- const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
722
- const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
723
- const colleagueContactReferenceDate = new Date();
724
- const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
1160
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1161
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
1162
+ const postActionEnabled = normalizedPostAction !== "none";
1163
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
1164
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
1165
+ const colleagueContactReferenceDate = new Date();
1166
+ const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
725
1167
  const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
726
1168
  const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
727
1169
  const networkRecorder = effectiveDetailLimit > 0
@@ -747,150 +1189,172 @@ export async function runRecommendWorkflow({
747
1189
  const results = [];
748
1190
  const refreshAttempts = [];
749
1191
  let refreshRounds = 0;
750
- let contextRecoveryAttempts = 0;
751
- let greetCount = 0;
1192
+ let contextRecoveryAttempts = 0;
1193
+ let greetCount = 0;
752
1194
  const candidateRecoveryCounts = new Map();
753
1195
  let jobSelection = null;
754
1196
  let pageScopeSelection = null;
1197
+ let currentCityOnlyResult = null;
755
1198
  let filterResult = null;
756
1199
  let rootState = null;
757
1200
  let cardNodeIds = [];
758
1201
  let listEndReason = "";
759
1202
  let lastHumanEvent = null;
760
- const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
1203
+ let debugForceReconnectPending = null;
1204
+ let listReadStaleRecoveryAttempts = 0;
1205
+ let listReadStaleRecoveryApplied = 0;
1206
+ let listReadStaleRecoveries = 0;
1207
+ let lastListReadStaleDiagnostic = null;
1208
+ let lastListReadRecoveryMode = null;
1209
+ const listReadStaleDiagnostics = [];
1210
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
761
1211
  rootNodeId: rootState?.iframe?.documentNodeId,
762
1212
  containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
763
1213
  itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
764
1214
  itemSelectors: [RECOMMEND_CARD_SELECTOR],
765
1215
  viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
766
1216
  validateViewportPoint: true
767
- }));
768
-
769
- function recordHumanEvent(event = null) {
770
- if (!event) return lastHumanEvent;
771
- lastHumanEvent = {
772
- at: new Date().toISOString(),
773
- ...event
774
- };
775
- return lastHumanEvent;
776
- }
777
-
778
- async function maybeHumanActionCooldown(phase, timings = {}) {
779
- if (!effectiveHumanBehavior.actionCooldown) return null;
780
- const pauseMs = humanDelay(280, 90, {
781
- minMs: 80,
782
- maxMs: 720
783
- });
784
- if (pauseMs > 0) {
785
- await runControl.sleep(pauseMs);
786
- addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
787
- }
788
- return recordHumanEvent({
789
- kind: "action_cooldown",
790
- phase,
791
- pause_ms: pauseMs
792
- });
793
- }
794
-
1217
+ }));
1218
+
1219
+ function recordHumanEvent(event = null) {
1220
+ if (!event) return lastHumanEvent;
1221
+ lastHumanEvent = {
1222
+ at: new Date().toISOString(),
1223
+ ...event
1224
+ };
1225
+ return lastHumanEvent;
1226
+ }
1227
+
1228
+ async function maybeHumanActionCooldown(phase, timings = {}) {
1229
+ if (!effectiveHumanBehavior.actionCooldown) return null;
1230
+ const pauseMs = humanDelay(280, 90, {
1231
+ minMs: 80,
1232
+ maxMs: 720
1233
+ });
1234
+ if (pauseMs > 0) {
1235
+ await runControl.sleep(pauseMs);
1236
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
1237
+ }
1238
+ return recordHumanEvent({
1239
+ kind: "action_cooldown",
1240
+ phase,
1241
+ pause_ms: pauseMs
1242
+ });
1243
+ }
1244
+
795
1245
  function updateRecommendProgress(extra = {}) {
796
1246
  const counts = countRecommendResultStatuses(results, { greetCount });
797
- const listSnapshot = compactInfiniteListState(listState);
798
- const humanRestState = humanRestController.getState();
799
- runControl.updateProgress({
800
- card_count: cardNodeIds.length,
1247
+ const listSnapshot = compactInfiniteListState(listState);
1248
+ const humanRestState = humanRestController.getState();
1249
+ runControl.updateProgress({
1250
+ card_count: cardNodeIds.length,
801
1251
  target_count: targetPassCount,
802
1252
  target_count_semantics: "passed_candidates",
803
- ...counts,
1253
+ ...counts,
1254
+ transient_recovered: counts.transient_recovered + listReadStaleRecoveries,
804
1255
  screening_mode: normalizedScreeningMode,
805
1256
  unique_seen: listSnapshot.seen_count,
806
1257
  scroll_count: listSnapshot.scroll_count,
807
1258
  refresh_rounds: refreshRounds,
808
1259
  refresh_attempts: refreshAttempts.length,
809
1260
  context_recoveries: contextRecoveryAttempts,
810
- list_end_reason: listEndReason || null,
811
- viewport_checks: viewportGuard.getStats().checks,
812
- viewport_recoveries: viewportGuard.getStats().recoveries,
813
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1261
+ list_end_reason: listEndReason || null,
1262
+ viewport_checks: viewportGuard.getStats().checks,
1263
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1264
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
814
1265
  human_behavior_profile: effectiveHumanBehavior.profile,
815
1266
  human_rest_level: effectiveHumanBehavior.restLevel,
816
1267
  human_rest_enabled: effectiveHumanRestEnabled,
817
1268
  skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
818
1269
  colleague_contact_window_days: normalizedColleagueContactWindowDays,
1270
+ current_city_only_requested: normalizedFilter.currentCityOnly,
1271
+ current_city_only_effective: currentCityOnlyResult?.effective ?? null,
1272
+ current_city_only_unavailable: Boolean(currentCityOnlyResult?.unavailable),
819
1273
  human_rest_count: humanRestState.rest_count,
820
1274
  human_rest_ms: humanRestState.total_rest_ms,
821
1275
  last_human_event: lastHumanEvent,
1276
+ debug_boundary_mode: debugBoundary.config.mode,
1277
+ debug_boundary_threshold: debugBoundary.config.threshold,
1278
+ debug_boundary_triggered: debugBoundary.getState().triggered,
1279
+ debug_boundary_trigger_count: debugBoundary.getState().trigger_count,
1280
+ list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
1281
+ list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
1282
+ list_read_stale_recoveries: listReadStaleRecoveries,
1283
+ last_list_read_recovery_mode: lastListReadRecoveryMode,
1284
+ last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
822
1285
  ...extra
823
1286
  });
824
1287
  }
825
1288
 
826
- function checkpointInProgressCandidate({
827
- index = results.length,
828
- candidateKey = "",
1289
+ function checkpointInProgressCandidate({
1290
+ index = results.length,
1291
+ candidateKey = "",
829
1292
  cardNodeId = null,
830
1293
  detailStep = "",
831
1294
  error = null
832
1295
  } = {}) {
833
1296
  runControl.checkpoint({
834
1297
  in_progress_candidate: {
835
- index,
836
- key: candidateKey,
837
- card_node_id: cardNodeId,
838
- detail_step: detailStep || null,
839
- counters: countRecommendResultStatuses(results, { greetCount }),
840
- error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
841
- },
842
- candidate_list: compactInfiniteListState(listState)
843
- });
844
- }
845
-
846
- async function closeRecommendBlockingPanelsForRun(phase = "cleanup") {
847
- const result = await closeRecommendBlockingPanels(client, {
848
- attemptsLimit: 2,
849
- rootState
850
- });
851
- if (!result?.closed) {
852
- throw createRecommendBlockingPanelCloseFailureError(result, phase);
853
- }
854
- return result;
855
- }
856
-
857
- async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
1298
+ index,
1299
+ key: candidateKey,
1300
+ card_node_id: cardNodeId,
1301
+ detail_step: detailStep || null,
1302
+ counters: countRecommendResultStatuses(results, { greetCount }),
1303
+ error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
1304
+ },
1305
+ candidate_list: compactInfiniteListState(listState)
1306
+ });
1307
+ }
1308
+
1309
+ async function closeRecommendBlockingPanelsForRun(phase = "cleanup") {
1310
+ const result = await closeRecommendBlockingPanels(client, {
1311
+ attemptsLimit: 2,
1312
+ rootState
1313
+ });
1314
+ if (!result?.closed) {
1315
+ throw createRecommendBlockingPanelCloseFailureError(result, phase);
1316
+ }
1317
+ return result;
1318
+ }
1319
+
1320
+ async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
858
1321
  forceRecentNotView = true
859
1322
  } = {}) {
860
- await runControl.waitIfPaused();
861
- runControl.throwIfCanceled();
862
- const started = Date.now();
863
- runControl.setPhase("recommend:recover-context");
864
- contextRecoveryAttempts += 1;
1323
+ await runControl.waitIfPaused();
1324
+ runControl.throwIfCanceled();
1325
+ const started = Date.now();
1326
+ runControl.setPhase("recommend:recover-context");
1327
+ contextRecoveryAttempts += 1;
1328
+ const effectiveForceRecentNotView = normalizedFilter.enabled && forceRecentNotView;
865
1329
  const refreshResult = await refreshRecommendListAtEnd(client, {
866
1330
  rootState,
867
1331
  jobLabel,
868
- pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
869
- fallbackPageScope: normalizedFallbackPageScope,
870
- filter: normalizedFilter,
871
- preferEndRefreshButton: false,
872
- forceNavigate: true,
873
- targetUrl: targetUrl || RECOMMEND_TARGET_URL,
874
- forceRecentNotView,
875
- cardTimeoutMs,
1332
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1333
+ fallbackPageScope: normalizedFallbackPageScope,
1334
+ filter: normalizedFilter,
1335
+ preferEndRefreshButton: false,
1336
+ forceNavigate: true,
1337
+ targetUrl: targetUrl || RECOMMEND_TARGET_URL,
1338
+ forceRecentNotView: effectiveForceRecentNotView,
1339
+ cardTimeoutMs,
876
1340
  buttonSettleMs: refreshButtonSettleMs,
877
1341
  reloadSettleMs: refreshReloadSettleMs
878
1342
  });
879
1343
  let blockingPanelClose = null;
880
- if (refreshResult.ok) {
881
- blockingPanelClose = await closeRecommendBlockingPanels(client, {
882
- attemptsLimit: 2,
883
- rootState: refreshResult.root_state || rootState
884
- });
885
- }
886
- const compactRefresh = {
887
- ...compactRefreshAttempt(refreshResult),
888
- context_recovery: true,
889
- recovery_reason: reason,
890
- trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
891
- account_rights_panel_close: compactCloseResult(blockingPanelClose),
892
- elapsed_ms: Date.now() - started
893
- };
1344
+ if (refreshResult.ok) {
1345
+ blockingPanelClose = await closeRecommendBlockingPanels(client, {
1346
+ attemptsLimit: 2,
1347
+ rootState: refreshResult.root_state || rootState
1348
+ });
1349
+ }
1350
+ const compactRefresh = {
1351
+ ...compactRefreshAttempt(refreshResult),
1352
+ context_recovery: true,
1353
+ recovery_reason: reason,
1354
+ trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
1355
+ account_rights_panel_close: compactCloseResult(blockingPanelClose),
1356
+ elapsed_ms: Date.now() - started
1357
+ };
894
1358
  refreshAttempts.push(compactRefresh);
895
1359
  runControl.checkpoint({
896
1360
  context_recovery: {
@@ -900,56 +1364,68 @@ export async function runRecommendWorkflow({
900
1364
  refresh: compactRefresh,
901
1365
  counters: countRecommendResultStatuses(results, { greetCount })
902
1366
  },
903
- candidate_list: compactInfiniteListState(listState)
904
- });
905
- if (!refreshResult.ok) {
906
- updateRecommendProgress({
907
- refresh_method: refreshResult.method || null,
908
- refresh_forced_recent_not_view: forceRecentNotView,
909
- recovery_reason: reason
1367
+ candidate_list: compactInfiniteListState(listState)
1368
+ });
1369
+ if (!refreshResult.ok) {
1370
+ updateRecommendProgress({
1371
+ refresh_method: refreshResult.method || null,
1372
+ refresh_forced_recent_not_view: effectiveForceRecentNotView,
1373
+ recovery_reason: reason
910
1374
  });
911
- throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
1375
+ const recoveryError = new Error(
1376
+ `Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`
1377
+ );
1378
+ recoveryError.code = "RECOMMEND_CONTEXT_RECOVERY_FAILED";
1379
+ recoveryError.refresh_attempt = compactRefresh;
1380
+ recoveryError.recovery_reason = reason;
1381
+ throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
1382
+ }
1383
+ if (refreshResult.current_city_only) {
1384
+ currentCityOnlyResult = refreshResult.current_city_only;
1385
+ }
1386
+ if (refreshResult.filter) {
1387
+ filterResult = refreshResult.filter;
912
1388
  }
913
1389
  if (!blockingPanelClose?.closed) {
914
1390
  const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
915
1391
  panelError.refresh_attempt = compactRefresh;
916
1392
  throw panelError;
917
1393
  }
918
- rootState = refreshResult.root_state || await getRecommendRoots(client);
1394
+ rootState = refreshResult.root_state || await getRecommendRoots(client);
919
1395
  rootState = await ensureRecommendViewport(rootState, "recover_after");
920
1396
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
921
1397
  timeoutMs: cardTimeoutMs,
922
1398
  intervalMs: 300
923
1399
  });
924
1400
  resetInfiniteListForRefreshRound(listState, {
925
- reason: `context_recovery:${reason}`,
926
- round: contextRecoveryAttempts,
927
- method: refreshResult.method,
928
- metadata: {
929
- card_count: cardNodeIds.length,
930
- forced_recent_not_view: forceRecentNotView,
931
- counters: countRecommendResultStatuses(results, { greetCount })
932
- }
933
- });
934
- listEndReason = "";
935
- updateRecommendProgress({
936
- card_count: cardNodeIds.length,
937
- refresh_method: refreshResult.method || null,
938
- refresh_forced_recent_not_view: forceRecentNotView,
939
- recovery_reason: reason
940
- });
941
- return refreshResult;
1401
+ reason: `context_recovery:${reason}`,
1402
+ round: contextRecoveryAttempts,
1403
+ method: refreshResult.method,
1404
+ metadata: {
1405
+ card_count: cardNodeIds.length,
1406
+ forced_recent_not_view: effectiveForceRecentNotView,
1407
+ counters: countRecommendResultStatuses(results, { greetCount })
1408
+ }
1409
+ });
1410
+ listEndReason = "";
1411
+ updateRecommendProgress({
1412
+ card_count: cardNodeIds.length,
1413
+ refresh_method: refreshResult.method || null,
1414
+ refresh_forced_recent_not_view: effectiveForceRecentNotView,
1415
+ recovery_reason: reason
1416
+ });
1417
+ return refreshResult;
942
1418
  }
943
1419
 
944
- runControl.setPhase("recommend:cleanup");
945
- await closeRecommendDetail(client, { attemptsLimit: 2 });
946
- await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
947
- await closeRecommendBlockingPanelsForRun("cleanup");
948
-
949
- await runControl.waitIfPaused();
950
- runControl.throwIfCanceled();
951
- runControl.setPhase("recommend:roots");
952
- rootState = await getRecommendRoots(client);
1420
+ runControl.setPhase("recommend:cleanup");
1421
+ await closeRecommendDetail(client, { attemptsLimit: 2 });
1422
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
1423
+ await closeRecommendBlockingPanelsForRun("cleanup");
1424
+
1425
+ await runControl.waitIfPaused();
1426
+ runControl.throwIfCanceled();
1427
+ runControl.setPhase("recommend:roots");
1428
+ rootState = await getRecommendRoots(client);
953
1429
  rootState = await ensureRecommendViewport(rootState, "roots");
954
1430
  runControl.checkpoint({
955
1431
  iframe_selector: rootState.iframe.selector,
@@ -987,34 +1463,55 @@ export async function runRecommendWorkflow({
987
1463
  throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
988
1464
  }
989
1465
  rootState = await getRecommendRoots(client);
990
- rootState = await ensureRecommendViewport(rootState, "page_scope");
991
- runControl.checkpoint({
992
- page_scope: compactPageScopeSelection(pageScopeSelection)
993
- });
994
-
995
- if (normalizedFilter.enabled) {
996
- await runControl.waitIfPaused();
997
- runControl.throwIfCanceled();
998
- runControl.setPhase("recommend:filter");
999
- filterResult = await selectAndConfirmFirstSafeFilter(
1000
- client,
1001
- rootState.iframe.documentNodeId,
1002
- buildRecommendFilterSelectionOptions(normalizedFilter)
1003
- );
1004
- if (!filterResult.confirmed) {
1005
- throw new Error("Recommend run filter selection was not confirmed");
1006
- }
1007
- rootState = await getRecommendRoots(client);
1008
- rootState = await ensureRecommendViewport(rootState, "filter");
1009
- runControl.checkpoint({
1010
- filter: compactFilterResult(filterResult)
1011
- });
1012
- }
1013
-
1014
- await runControl.waitIfPaused();
1015
- runControl.throwIfCanceled();
1016
- runControl.setPhase("recommend:cards");
1017
- rootState = await ensureRecommendViewport(rootState, "cards");
1466
+ rootState = await ensureRecommendViewport(rootState, "page_scope");
1467
+ runControl.checkpoint({
1468
+ page_scope: compactPageScopeSelection(pageScopeSelection)
1469
+ });
1470
+
1471
+ const initialFilterStages = await applyRecommendFilterEnvelopeStages(normalizedFilter, {
1472
+ applyCurrentCityOnly: async () => {
1473
+ await runControl.waitIfPaused();
1474
+ runControl.throwIfCanceled();
1475
+ runControl.setPhase("recommend:current-city-only");
1476
+ const result = await ensureRecommendCurrentCityOnly(
1477
+ client,
1478
+ rootState.iframe.documentNodeId,
1479
+ { enabled: normalizedFilter.currentCityOnly }
1480
+ );
1481
+ rootState = await getRecommendRoots(client);
1482
+ rootState = await ensureRecommendViewport(rootState, "current_city_only");
1483
+ runControl.checkpoint({
1484
+ current_city_only: compactCurrentCityOnlyResult(result)
1485
+ });
1486
+ return result;
1487
+ },
1488
+ applyFilterPanel: async () => {
1489
+ await runControl.waitIfPaused();
1490
+ runControl.throwIfCanceled();
1491
+ runControl.setPhase("recommend:filter");
1492
+ const result = await selectAndConfirmFirstSafeFilter(
1493
+ client,
1494
+ rootState.iframe.documentNodeId,
1495
+ buildRecommendFilterSelectionOptions(normalizedFilter)
1496
+ );
1497
+ if (!result.confirmed) {
1498
+ throw new Error("Recommend run filter selection was not confirmed");
1499
+ }
1500
+ rootState = await getRecommendRoots(client);
1501
+ rootState = await ensureRecommendViewport(rootState, "filter");
1502
+ runControl.checkpoint({
1503
+ filter: compactFilterResult(result)
1504
+ });
1505
+ return result;
1506
+ }
1507
+ });
1508
+ currentCityOnlyResult = initialFilterStages.current_city_only;
1509
+ filterResult = initialFilterStages.filter;
1510
+
1511
+ await runControl.waitIfPaused();
1512
+ runControl.throwIfCanceled();
1513
+ runControl.setPhase("recommend:cards");
1514
+ rootState = await ensureRecommendViewport(rootState, "cards");
1018
1515
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1019
1516
  timeoutMs: cardTimeoutMs,
1020
1517
  intervalMs: 300
@@ -1027,50 +1524,247 @@ export async function runRecommendWorkflow({
1027
1524
  list_end_reason: null
1028
1525
  });
1029
1526
 
1030
- while (countPassedResults(results) < targetPassCount) {
1031
- const candidateStarted = Date.now();
1032
- const timings = {};
1033
- await runControl.waitIfPaused();
1034
- runControl.throwIfCanceled();
1035
- runControl.setPhase("recommend:candidate");
1036
- rootState = await ensureRecommendViewport(rootState, "candidate_loop");
1037
-
1038
- const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1039
- client,
1040
- state: listState,
1041
- maxScrolls: listMaxScrolls,
1042
- stableSignatureLimit: listStableSignatureLimit,
1043
- wheelDeltaY: listWheelDeltaY,
1044
- settleMs: listSettleMs,
1045
- listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1046
- fallbackPoint: listFallbackResolver,
1047
- findNodeIds: async () => {
1048
- let currentRootState = await getRecommendRoots(client);
1049
- currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
1050
- rootState = currentRootState;
1051
- const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
1052
- timeoutMs: Math.min(cardTimeoutMs, 5000),
1053
- intervalMs: 300
1054
- });
1055
- cardNodeIds = currentCardNodeIds;
1056
- return currentCardNodeIds;
1057
- },
1058
- readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
1059
- targetUrl,
1060
- source: "recommend-run-card",
1061
- metadata: {
1062
- run_candidate_index: results.length,
1063
- visible_index: visibleIndex
1064
- }
1065
- }),
1066
- detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1067
- rootNodeId: rootState?.iframe?.documentNodeId,
1068
- markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
1069
- refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
1070
- textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1071
- maxTextScanNodes: 500
1072
- })
1073
- }));
1527
+ while (countPassedResults(results) < targetPassCount) {
1528
+ const candidateStarted = Date.now();
1529
+ const timings = {};
1530
+ await runControl.waitIfPaused();
1531
+ runControl.throwIfCanceled();
1532
+ runControl.setPhase("recommend:list-read");
1533
+ const debugBoundaryAction = debugBoundary.take(results.length);
1534
+ let debugForcedListEnd = false;
1535
+ if (debugBoundaryAction) {
1536
+ const debugProgress = {
1537
+ debug_boundary_mode: debugBoundaryAction.mode,
1538
+ debug_boundary_threshold: debugBoundaryAction.threshold,
1539
+ debug_boundary_triggered: true,
1540
+ debug_boundary_trigger_count: debugBoundaryAction.trigger_count,
1541
+ debug_boundary_processed: debugBoundaryAction.processed
1542
+ };
1543
+ updateRecommendProgress(debugProgress);
1544
+ runControl.checkpoint({
1545
+ debug_boundary: {
1546
+ ...debugProgress,
1547
+ field: debugBoundaryAction.field,
1548
+ triggered_at: new Date().toISOString()
1549
+ }
1550
+ });
1551
+ if (debugBoundaryAction.mode === "context_recovery") {
1552
+ runControl.setPhase("recommend:debug-force-context-recovery");
1553
+ await recoverAndReapplyRecommendContext("debug_force_context_recovery_after_processed", null, {
1554
+ forceRecentNotView: true
1555
+ });
1556
+ updateRecommendProgress({
1557
+ debug_force_context_recovery_count: debugBoundaryAction.trigger_count
1558
+ });
1559
+ runControl.setPhase("recommend:list-read");
1560
+ } else if (debugBoundaryAction.mode === "cdp_reconnect") {
1561
+ debugForceReconnectPending = debugBoundaryAction;
1562
+ updateRecommendProgress({
1563
+ debug_force_cdp_reconnect_pending: true
1564
+ });
1565
+ } else if (debugBoundaryAction.mode === "list_end") {
1566
+ debugForcedListEnd = true;
1567
+ updateRecommendProgress({
1568
+ debug_force_list_end_count: debugBoundaryAction.trigger_count
1569
+ });
1570
+ }
1571
+ }
1572
+ const listReadAcquisition = await acquireRecommendListReadWithStaleRecovery({
1573
+ maxRetries: 2,
1574
+ acquire: async () => {
1575
+ await runControl.waitIfPaused();
1576
+ runControl.throwIfCanceled();
1577
+ runControl.setPhase("recommend:list-read");
1578
+ rootState = await ensureRecommendViewport(rootState, "candidate_loop");
1579
+ if (debugForcedListEnd) {
1580
+ return {
1581
+ ok: false,
1582
+ end_reached: true,
1583
+ reason: "debug_forced_list_end"
1584
+ };
1585
+ }
1586
+ return measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1587
+ client,
1588
+ state: listState,
1589
+ maxScrolls: listMaxScrolls,
1590
+ stableSignatureLimit: listStableSignatureLimit,
1591
+ wheelDeltaY: listWheelDeltaY,
1592
+ settleMs: listSettleMs,
1593
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1594
+ fallbackPoint: listFallbackResolver,
1595
+ findNodeIds: async () => {
1596
+ let currentRootState = await getRecommendRoots(client);
1597
+ currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
1598
+ rootState = currentRootState;
1599
+ if (debugForceReconnectPending) {
1600
+ runControl.setPhase("recommend:debug-force-cdp-reconnect");
1601
+ const rawClient = client.__rawClient;
1602
+ if (!rawClient || typeof rawClient.close !== "function") {
1603
+ const error = new Error("Guarded CDP client does not expose a closable __rawClient");
1604
+ error.code = "RECOMMEND_DEBUG_RAW_CDP_UNAVAILABLE";
1605
+ throw error;
1606
+ }
1607
+ const forcedReconnectAction = debugForceReconnectPending;
1608
+ debugForceReconnectPending = null;
1609
+ await rawClient.close();
1610
+ runControl.checkpoint({
1611
+ debug_cdp_reconnect: {
1612
+ raw_connection_closed: true,
1613
+ processed: forcedReconnectAction.processed,
1614
+ threshold: forcedReconnectAction.threshold,
1615
+ closed_at: new Date().toISOString()
1616
+ }
1617
+ });
1618
+ updateRecommendProgress({
1619
+ debug_force_cdp_reconnect_pending: false,
1620
+ debug_force_cdp_reconnect_count: forcedReconnectAction.trigger_count
1621
+ });
1622
+ runControl.setPhase("recommend:list-read");
1623
+ }
1624
+ const currentCardNodeIds = await waitForRecommendCardNodeIds(
1625
+ client,
1626
+ currentRootState.iframe.documentNodeId,
1627
+ {
1628
+ timeoutMs: Math.min(cardTimeoutMs, 5000),
1629
+ intervalMs: 300
1630
+ }
1631
+ );
1632
+ cardNodeIds = currentCardNodeIds;
1633
+ return currentCardNodeIds;
1634
+ },
1635
+ readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
1636
+ targetUrl,
1637
+ source: "recommend-run-card",
1638
+ metadata: {
1639
+ run_candidate_index: results.length,
1640
+ visible_index: visibleIndex
1641
+ }
1642
+ }),
1643
+ shouldRethrowReadError: isStaleRecommendNodeError,
1644
+ detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1645
+ rootNodeId: rootState?.iframe?.documentNodeId,
1646
+ markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
1647
+ refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
1648
+ textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1649
+ maxTextScanNodes: 500
1650
+ })
1651
+ }));
1652
+ },
1653
+ onStale: async ({ diagnostic }) => {
1654
+ listReadStaleRecoveryAttempts += 1;
1655
+ lastListReadStaleDiagnostic = diagnostic;
1656
+ listReadStaleDiagnostics.push(diagnostic);
1657
+ runControl.checkpoint({
1658
+ list_read_stale_recovery: {
1659
+ diagnostic,
1660
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1661
+ counters: countRecommendResultStatuses(results, { greetCount }),
1662
+ candidate_list: compactInfiniteListState(listState)
1663
+ }
1664
+ });
1665
+ updateRecommendProgress();
1666
+ },
1667
+ recover: async ({ error, diagnostic }) => {
1668
+ try {
1669
+ return await recoverRecommendListReadStaleContext({
1670
+ staleAttempt: diagnostic.attempt,
1671
+ listState,
1672
+ rootReacquire: async () => {
1673
+ await runControl.waitIfPaused();
1674
+ runControl.throwIfCanceled();
1675
+ runControl.setPhase("recommend:list-read-root-reacquire");
1676
+ let freshRootState = await getRecommendRoots(client);
1677
+ freshRootState = await ensureRecommendViewport(
1678
+ freshRootState,
1679
+ "list_read_stale_root_reacquire"
1680
+ );
1681
+ const freshCardNodeIds = await waitForRecommendCardNodeIds(
1682
+ client,
1683
+ freshRootState.iframe.documentNodeId,
1684
+ {
1685
+ timeoutMs: Math.min(cardTimeoutMs, 10000),
1686
+ intervalMs: 300
1687
+ }
1688
+ );
1689
+ rootState = freshRootState;
1690
+ cardNodeIds = freshCardNodeIds;
1691
+ return {
1692
+ card_count: freshCardNodeIds.length,
1693
+ iframe_document_node_id: freshRootState.iframe.documentNodeId
1694
+ };
1695
+ },
1696
+ contextReapply: async ({ rootReacquireError }) => {
1697
+ await runControl.waitIfPaused();
1698
+ runControl.throwIfCanceled();
1699
+ const recovery = await recoverAndReapplyRecommendContext(
1700
+ "list_read_stale_node",
1701
+ rootReacquireError || error,
1702
+ { forceRecentNotView: true }
1703
+ );
1704
+ return {
1705
+ ok: Boolean(recovery?.ok),
1706
+ method: recovery?.method || null,
1707
+ card_count: recovery?.card_count || 0
1708
+ };
1709
+ }
1710
+ });
1711
+ } catch (recoveryError) {
1712
+ updateRecommendProgress({
1713
+ list_read_stale_recovery_failed: true
1714
+ });
1715
+ runControl.checkpoint({
1716
+ list_read_stale_recovery_failed: {
1717
+ trigger: lastListReadStaleDiagnostic,
1718
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1719
+ recovery_error: compactError(recoveryError, "RECOMMEND_LIST_READ_RECOVERY_FAILED"),
1720
+ candidate_list: compactInfiniteListState(listState)
1721
+ }
1722
+ });
1723
+ throw recoveryError;
1724
+ }
1725
+ },
1726
+ onRecoveryApplied: async ({ diagnostic }) => {
1727
+ listReadStaleRecoveryApplied += 1;
1728
+ lastListReadStaleDiagnostic = diagnostic;
1729
+ lastListReadRecoveryMode = diagnostic.recovery_mode || null;
1730
+ updateRecommendProgress();
1731
+ runControl.checkpoint({
1732
+ list_read_stale_recovery_applied: {
1733
+ diagnostic,
1734
+ recovery_applied_count: listReadStaleRecoveryApplied,
1735
+ candidate_list: compactInfiniteListState(listState)
1736
+ }
1737
+ });
1738
+ },
1739
+ onRecovered: async ({ diagnostic }) => {
1740
+ listReadStaleRecoveries += 1;
1741
+ lastListReadStaleDiagnostic = diagnostic;
1742
+ updateRecommendProgress();
1743
+ runControl.checkpoint({
1744
+ list_read_stale_recovered: {
1745
+ diagnostic,
1746
+ recovery_count: listReadStaleRecoveries,
1747
+ candidate_list: compactInfiniteListState(listState)
1748
+ }
1749
+ });
1750
+ },
1751
+ onExhausted: async ({ diagnostic }) => {
1752
+ listReadStaleRecoveryAttempts += 1;
1753
+ lastListReadStaleDiagnostic = diagnostic;
1754
+ listReadStaleDiagnostics.push(diagnostic);
1755
+ updateRecommendProgress({
1756
+ list_read_stale_recovery_exhausted: true
1757
+ });
1758
+ runControl.checkpoint({
1759
+ list_read_stale_recovery_exhausted: {
1760
+ diagnostic,
1761
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1762
+ candidate_list: compactInfiniteListState(listState)
1763
+ }
1764
+ });
1765
+ }
1766
+ });
1767
+ const nextCandidateResult = listReadAcquisition.result;
1074
1768
  if (!nextCandidateResult.ok) {
1075
1769
  listEndReason = nextCandidateResult.reason || "list_exhausted";
1076
1770
  if (
@@ -1083,99 +1777,107 @@ export async function runRecommendWorkflow({
1083
1777
  runControl.throwIfCanceled();
1084
1778
  runControl.setPhase("recommend:refresh");
1085
1779
  refreshRounds += 1;
1086
- const refreshResult = await refreshRecommendListAtEnd(client, {
1087
- rootState,
1088
- jobLabel,
1089
- pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1090
- fallbackPageScope: normalizedFallbackPageScope,
1091
- filter: normalizedFilter,
1092
- forceRecentNotView: true,
1093
- cardTimeoutMs,
1094
- buttonSettleMs: refreshButtonSettleMs,
1095
- reloadSettleMs: refreshReloadSettleMs
1096
- });
1097
- const compactRefresh = compactRefreshAttempt(refreshResult);
1780
+ const refreshResult = await refreshRecommendListAtEnd(client, {
1781
+ rootState,
1782
+ jobLabel,
1783
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1784
+ fallbackPageScope: normalizedFallbackPageScope,
1785
+ filter: normalizedFilter,
1786
+ preferEndRefreshButton: debugForcedListEnd ? false : undefined,
1787
+ forceRecentNotView: normalizedFilter.enabled,
1788
+ cardTimeoutMs,
1789
+ buttonSettleMs: refreshButtonSettleMs,
1790
+ reloadSettleMs: refreshReloadSettleMs
1791
+ });
1792
+ const compactRefresh = compactRefreshAttempt(refreshResult);
1098
1793
  refreshAttempts.push(compactRefresh);
1099
1794
  runControl.checkpoint({
1100
1795
  refresh_round: refreshRounds,
1101
- refresh: compactRefresh
1102
- });
1103
- updateRecommendProgress({
1104
- card_count: refreshResult.card_count || cardNodeIds.length,
1105
- refresh_method: refreshResult.method || null,
1106
- refresh_forced_recent_not_view: true,
1107
- list_end_reason: listEndReason
1108
- });
1796
+ refresh: compactRefresh
1797
+ });
1798
+ updateRecommendProgress({
1799
+ card_count: refreshResult.card_count || cardNodeIds.length,
1800
+ refresh_method: refreshResult.method || null,
1801
+ refresh_forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view),
1802
+ list_end_reason: listEndReason
1803
+ });
1109
1804
  if (refreshResult.ok) {
1805
+ if (refreshResult.current_city_only) {
1806
+ currentCityOnlyResult = refreshResult.current_city_only;
1807
+ }
1808
+ if (refreshResult.filter) {
1809
+ filterResult = refreshResult.filter;
1810
+ }
1110
1811
  rootState = refreshResult.root_state || await getRecommendRoots(client);
1111
1812
  rootState = await ensureRecommendViewport(rootState, "refresh_after");
1112
- cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1113
- timeoutMs: cardTimeoutMs,
1114
- intervalMs: 300
1813
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1814
+ timeoutMs: cardTimeoutMs,
1815
+ intervalMs: 300
1115
1816
  });
1116
1817
  resetInfiniteListForRefreshRound(listState, {
1117
- reason: listEndReason,
1118
- round: refreshRounds,
1119
- method: refreshResult.method,
1120
- metadata: {
1121
- card_count: cardNodeIds.length,
1122
- forced_recent_not_view: true
1123
- }
1124
- });
1818
+ reason: listEndReason,
1819
+ round: refreshRounds,
1820
+ method: refreshResult.method,
1821
+ metadata: {
1822
+ card_count: cardNodeIds.length,
1823
+ forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view)
1824
+ }
1825
+ });
1125
1826
  listEndReason = "";
1126
1827
  continue;
1127
1828
  }
1128
- throw createRecommendRefreshFailureError(compactRefresh, {
1129
- listEndReason,
1130
- targetCount: targetPassCount,
1131
- passedCount: countPassedResults(results)
1132
- });
1133
- }
1134
- break;
1135
- }
1829
+ throw createRecommendRefreshFailureError(compactRefresh, {
1830
+ listEndReason,
1831
+ targetCount: targetPassCount,
1832
+ passedCount: countPassedResults(results)
1833
+ });
1834
+ }
1835
+ break;
1836
+ }
1136
1837
 
1137
- const index = results.length;
1838
+ runControl.setPhase("recommend:candidate");
1839
+ const index = results.length;
1138
1840
  let cardNodeId = nextCandidateResult.item.node_id;
1139
1841
  const candidateKey = nextCandidateResult.item.key;
1140
1842
  let cardCandidate = nextCandidateResult.item.candidate;
1141
1843
 
1142
1844
  let screeningCandidate = cardCandidate;
1143
- let detailResult = null;
1144
- let recoverableDetailError = null;
1145
- let colleagueContact = null;
1146
- let skipRecentColleagueContact = false;
1147
- let detailStep = "not_started";
1845
+ let detailResult = null;
1846
+ let recoverableDetailError = null;
1847
+ let colleagueContact = null;
1848
+ let skipRecentColleagueContact = false;
1849
+ let detailStep = "not_started";
1148
1850
  if (index < effectiveDetailLimit) {
1149
1851
  try {
1150
1852
  await runControl.waitIfPaused();
1151
1853
  runControl.throwIfCanceled();
1152
- runControl.setPhase("recommend:detail");
1153
- detailStep = "ensure_viewport";
1154
- rootState = await ensureRecommendViewport(rootState, "detail");
1155
- const blockingPanelClose = await closeRecommendBlockingPanels(client, {
1156
- attemptsLimit: 2,
1157
- rootState
1158
- });
1159
- if (!blockingPanelClose?.closed) {
1160
- const panelError = createRecommendBlockingPanelCloseFailureError(
1161
- blockingPanelClose,
1162
- "before_detail_open"
1163
- );
1164
- timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1165
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
1166
- await recoverAndReapplyRecommendContext("account_rights_panel_before_detail", panelError, {
1167
- forceRecentNotView: true
1168
- });
1169
- continue;
1170
- }
1171
- if (blockingPanelClose.already_closed === false) {
1172
- timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1173
- }
1174
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1175
- detailStep = "open_detail";
1176
- networkRecorder.clear();
1177
- await maybeHumanActionCooldown("before_detail_open", timings);
1178
- const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1854
+ runControl.setPhase("recommend:detail");
1855
+ detailStep = "ensure_viewport";
1856
+ rootState = await ensureRecommendViewport(rootState, "detail");
1857
+ const blockingPanelClose = await closeRecommendBlockingPanels(client, {
1858
+ attemptsLimit: 2,
1859
+ rootState
1860
+ });
1861
+ if (!blockingPanelClose?.closed) {
1862
+ const panelError = createRecommendBlockingPanelCloseFailureError(
1863
+ blockingPanelClose,
1864
+ "before_detail_open"
1865
+ );
1866
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1867
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
1868
+ await recoverAndReapplyRecommendContext("account_rights_panel_before_detail", panelError, {
1869
+ forceRecentNotView: true
1870
+ });
1871
+ continue;
1872
+ }
1873
+ if (blockingPanelClose.already_closed === false) {
1874
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1875
+ }
1876
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1877
+ detailStep = "open_detail";
1878
+ networkRecorder.clear();
1879
+ await maybeHumanActionCooldown("before_detail_open", timings);
1880
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1179
1881
  cardNodeId,
1180
1882
  candidateKey,
1181
1883
  cardCandidate,
@@ -1186,52 +1888,52 @@ export async function runRecommendWorkflow({
1186
1888
  });
1187
1889
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
1188
1890
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
1189
- cardNodeId = openedDetail.card_node_id || cardNodeId;
1190
- cardCandidate = openedDetail.card_candidate || cardCandidate;
1191
- screeningCandidate = cardCandidate;
1192
- if (shouldSkipRecentColleagueContacted) {
1193
- detailStep = "check_colleague_contact";
1194
- try {
1195
- colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
1196
- client,
1197
- openedDetail.detail_state,
1198
- {
1199
- referenceDate: colleagueContactReferenceDate,
1200
- windowDays: normalizedColleagueContactWindowDays
1201
- }
1202
- ));
1203
- if (colleagueContact?.recent) {
1204
- skipRecentColleagueContact = true;
1205
- detailResult = {
1206
- candidate: screeningCandidate,
1207
- detail: {
1208
- popup_text: "",
1209
- resume_text: ""
1210
- },
1211
- colleague_contact: colleagueContact,
1212
- cv_acquisition: {
1213
- source: "skipped_recent_colleague_contact",
1214
- skipped: true,
1215
- reason: "skipped_recent_colleague_contact"
1216
- }
1217
- };
1218
- detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1219
- await maybeHumanActionCooldown("after_detail_close", timings);
1220
- }
1221
- } catch (error) {
1222
- colleagueContact = {
1223
- checked: false,
1224
- recent: false,
1225
- reason: "inspection_failed",
1226
- error: error?.message || String(error),
1227
- window_days: normalizedColleagueContactWindowDays
1228
- };
1229
- }
1230
- }
1231
- if (!skipRecentColleagueContact) {
1232
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1233
- detailStep = "wait_network";
1234
- const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1891
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
1892
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
1893
+ screeningCandidate = cardCandidate;
1894
+ if (shouldSkipRecentColleagueContacted) {
1895
+ detailStep = "check_colleague_contact";
1896
+ try {
1897
+ colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
1898
+ client,
1899
+ openedDetail.detail_state,
1900
+ {
1901
+ referenceDate: colleagueContactReferenceDate,
1902
+ windowDays: normalizedColleagueContactWindowDays
1903
+ }
1904
+ ));
1905
+ if (colleagueContact?.recent) {
1906
+ skipRecentColleagueContact = true;
1907
+ detailResult = {
1908
+ candidate: screeningCandidate,
1909
+ detail: {
1910
+ popup_text: "",
1911
+ resume_text: ""
1912
+ },
1913
+ colleague_contact: colleagueContact,
1914
+ cv_acquisition: {
1915
+ source: "skipped_recent_colleague_contact",
1916
+ skipped: true,
1917
+ reason: "skipped_recent_colleague_contact"
1918
+ }
1919
+ };
1920
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1921
+ await maybeHumanActionCooldown("after_detail_close", timings);
1922
+ }
1923
+ } catch (error) {
1924
+ colleagueContact = {
1925
+ checked: false,
1926
+ recent: false,
1927
+ reason: "inspection_failed",
1928
+ error: error?.message || String(error),
1929
+ window_days: normalizedColleagueContactWindowDays
1930
+ };
1931
+ }
1932
+ }
1933
+ if (!skipRecentColleagueContact) {
1934
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1935
+ detailStep = "wait_network";
1936
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1235
1937
  waitForRecommendDetailNetworkEvents,
1236
1938
  networkRecorder,
1237
1939
  {
@@ -1255,10 +1957,10 @@ export async function runRecommendWorkflow({
1255
1957
  networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1256
1958
  networkParseIntervalMs: 250
1257
1959
  });
1258
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1259
- if (colleagueContact) detailResult.colleague_contact = colleagueContact;
1260
-
1261
- const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1960
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1961
+ if (colleagueContact) detailResult.colleague_contact = colleagueContact;
1962
+
1963
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1262
1964
  let source = "network";
1263
1965
  let imageEvidence = null;
1264
1966
  let captureTarget = null;
@@ -1295,12 +1997,12 @@ export async function runRecommendWorkflow({
1295
1997
  resizeMaxWidth: 1100,
1296
1998
  captureViewport: false,
1297
1999
  padding: 0,
1298
- maxScreenshots: maxImagePages,
1299
- wheelDeltaY: imageWheelDeltaY,
1300
- settleMs: 350,
1301
- scrollMethod: "dom-anchor-fallback-input",
1302
- scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1303
- stepTimeoutMs: 45000,
2000
+ maxScreenshots: maxImagePages,
2001
+ wheelDeltaY: imageWheelDeltaY,
2002
+ settleMs: 350,
2003
+ scrollMethod: "dom-anchor-fallback-input",
2004
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
2005
+ stepTimeoutMs: 45000,
1304
2006
  totalTimeoutMs: 90000,
1305
2007
  duplicateStopCount: 1,
1306
2008
  skipDuplicateScreenshots: true,
@@ -1324,14 +2026,14 @@ export async function runRecommendWorkflow({
1324
2026
  const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1325
2027
  if (recoveryCount < 1) {
1326
2028
  candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1327
- timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
1328
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1329
- await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1330
- await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1331
- await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1332
- await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
1333
- forceRecentNotView: true
1334
- });
2029
+ timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
2030
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
2031
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
2032
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
2033
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
2034
+ await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
2035
+ forceRecentNotView: true
2036
+ });
1335
2037
  continue;
1336
2038
  }
1337
2039
  imageEvidence = createRecoverableImageCaptureEvidence(error, {
@@ -1370,73 +2072,73 @@ export async function runRecommendWorkflow({
1370
2072
  image_evidence: summarizeImageEvidence(imageEvidence),
1371
2073
  capture_target: captureTarget || null,
1372
2074
  capture_target_wait: captureTargetWait
1373
- };
1374
- screeningCandidate = detailResult.candidate;
1375
- }
1376
- } catch (error) {
2075
+ };
2076
+ screeningCandidate = detailResult.candidate;
2077
+ }
2078
+ } catch (error) {
1377
2079
  if (!isRecoverableRecommendDetailError(error)) throw error;
1378
2080
  const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1379
2081
  if (recoveryCount < 1) {
1380
- candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1381
- timings.detail_recovery_trigger = compactRecoverableDetailError(error);
1382
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1383
- await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1384
- await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1385
- await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1386
- await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
1387
- forceRecentNotView: true
1388
- });
2082
+ candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
2083
+ timings.detail_recovery_trigger = compactRecoverableDetailError(error);
2084
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
2085
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
2086
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
2087
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
2088
+ await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
2089
+ forceRecentNotView: true
2090
+ });
1389
2091
  continue;
1390
2092
  }
1391
- recoverableDetailError = error;
1392
- detailResult = null;
1393
- timings.detail_recovered_error = compactRecoverableDetailError(error);
1394
- await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1395
- await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1396
- await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1397
- }
2093
+ recoverableDetailError = error;
2094
+ detailResult = null;
2095
+ timings.detail_recovered_error = compactRecoverableDetailError(error);
2096
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
2097
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
2098
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
2099
+ }
1398
2100
  }
1399
2101
 
1400
2102
  await runControl.waitIfPaused();
1401
2103
  runControl.throwIfCanceled();
1402
2104
  runControl.setPhase("recommend:screening");
1403
- let llmResult = null;
1404
- if (useLlmScreening) {
1405
- if (skipRecentColleagueContact || recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1406
- llmResult = null;
1407
- } else if (!llmConfig) {
2105
+ let llmResult = null;
2106
+ if (useLlmScreening) {
2107
+ if (skipRecentColleagueContact || recoverableDetailError || detailResult?.image_evidence?.ok === false) {
2108
+ llmResult = null;
2109
+ } else if (!llmConfig) {
1408
2110
  llmResult = createMissingLlmConfigResult();
1409
2111
  } else {
1410
2112
  try {
1411
2113
  const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
1412
2114
  ? "vision_model_ms"
1413
2115
  : "text_model_ms";
1414
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1415
- candidate: screeningCandidate,
1416
- criteria,
1417
- config: llmConfig,
1418
- timeoutMs: llmTimeoutMs,
2116
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
2117
+ candidate: screeningCandidate,
2118
+ criteria,
2119
+ config: llmConfig,
2120
+ timeoutMs: llmTimeoutMs,
1419
2121
  imageEvidence: detailResult?.image_evidence || null,
1420
2122
  maxImages: llmImageLimit,
1421
2123
  imageDetail: llmImageDetail
1422
2124
  }));
1423
- } catch (error) {
1424
- if (isFatalLlmProviderError(error)) {
1425
- throw createFatalLlmRunError(error, {
1426
- domain: "recommend",
1427
- candidate: screeningCandidate
1428
- });
1429
- }
1430
- llmResult = createFailedLlmScreeningResult(error);
1431
- }
1432
- }
1433
- if (detailResult) detailResult.llm_result = llmResult;
1434
- }
1435
- const screening = skipRecentColleagueContact
1436
- ? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
1437
- : recoverableDetailError
1438
- ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1439
- : detailResult?.image_evidence?.ok === false
2125
+ } catch (error) {
2126
+ if (isFatalLlmProviderError(error)) {
2127
+ throw createFatalLlmRunError(error, {
2128
+ domain: "recommend",
2129
+ candidate: screeningCandidate
2130
+ });
2131
+ }
2132
+ llmResult = createFailedLlmScreeningResult(error);
2133
+ }
2134
+ }
2135
+ if (detailResult) detailResult.llm_result = llmResult;
2136
+ }
2137
+ const screening = skipRecentColleagueContact
2138
+ ? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
2139
+ : recoverableDetailError
2140
+ ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
2141
+ : detailResult?.image_evidence?.ok === false
1440
2142
  ? createImageCaptureFailureScreening(screeningCandidate, {
1441
2143
  code: detailResult.image_evidence.error_code,
1442
2144
  message: detailResult.image_evidence.error
@@ -1444,17 +2146,17 @@ export async function runRecommendWorkflow({
1444
2146
  : useLlmScreening
1445
2147
  ? llmResultToScreening(llmResult, screeningCandidate)
1446
2148
  : screenCandidate(screeningCandidate, { criteria });
1447
- let actionDiscovery = null;
1448
- let postActionResult = null;
1449
- let closeFailureError = null;
1450
- let closeRecoveryFailure = null;
1451
- if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
1452
- const postActionStarted = Date.now();
1453
- await runControl.waitIfPaused();
1454
- runControl.throwIfCanceled();
1455
- runControl.setPhase("recommend:post-action");
1456
- await maybeHumanActionCooldown("before_post_action", timings);
1457
- actionDiscovery = await waitForRecommendDetailActionControls(client, {
2149
+ let actionDiscovery = null;
2150
+ let postActionResult = null;
2151
+ let closeFailureError = null;
2152
+ let closeRecoveryFailure = null;
2153
+ if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
2154
+ const postActionStarted = Date.now();
2155
+ await runControl.waitIfPaused();
2156
+ runControl.throwIfCanceled();
2157
+ runControl.setPhase("recommend:post-action");
2158
+ await maybeHumanActionCooldown("before_post_action", timings);
2159
+ actionDiscovery = await waitForRecommendDetailActionControls(client, {
1458
2160
  timeoutMs: actionTimeoutMs,
1459
2161
  intervalMs: actionIntervalMs,
1460
2162
  requireAny: true
@@ -1475,32 +2177,33 @@ export async function runRecommendWorkflow({
1475
2177
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1476
2178
  }
1477
2179
  if (detailResult && closeDetail && !detailResult.close_result?.closed) {
2180
+ runControl.setPhase("recommend:close-detail");
1478
2181
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1479
- await maybeHumanActionCooldown("after_detail_close", timings);
1480
- if (!detailResult.close_result?.closed) {
1481
- closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
1482
- try {
1483
- const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeFailureError, {
1484
- forceRecentNotView: true
1485
- });
1486
- detailResult.cv_acquisition = {
1487
- ...(detailResult.cv_acquisition || {}),
1488
- close_recovery: {
1489
- ok: Boolean(recovery.ok),
1490
- method: recovery.method || "",
1491
- forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
1492
- card_count: recovery.card_count || 0
1493
- }
1494
- };
1495
- } catch (error) {
1496
- closeRecoveryFailure = error;
1497
- detailResult.cv_acquisition = {
2182
+ await maybeHumanActionCooldown("after_detail_close", timings);
2183
+ if (!detailResult.close_result?.closed) {
2184
+ closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
2185
+ try {
2186
+ const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeFailureError, {
2187
+ forceRecentNotView: true
2188
+ });
2189
+ detailResult.cv_acquisition = {
2190
+ ...(detailResult.cv_acquisition || {}),
2191
+ close_recovery: {
2192
+ ok: Boolean(recovery.ok),
2193
+ method: recovery.method || "",
2194
+ forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
2195
+ card_count: recovery.card_count || 0
2196
+ }
2197
+ };
2198
+ } catch (error) {
2199
+ closeRecoveryFailure = error;
2200
+ detailResult.cv_acquisition = {
1498
2201
  ...(detailResult.cv_acquisition || {}),
1499
2202
  close_recovery: {
1500
2203
  ok: false,
1501
2204
  reason: "context_recovery_failed",
1502
2205
  error: error?.message || String(error),
1503
- forced_recent_not_view: true
2206
+ forced_recent_not_view: Boolean(normalizedFilter.enabled)
1504
2207
  }
1505
2208
  };
1506
2209
  }
@@ -1516,14 +2219,14 @@ export async function runRecommendWorkflow({
1516
2219
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1517
2220
  screening: compactScreening(screening),
1518
2221
  action_discovery: compactActionDiscovery(actionDiscovery),
1519
- post_action: postActionResult,
1520
- error: recoverableDetailError
1521
- ? compactRecoverableDetailError(recoverableDetailError)
1522
- : closeRecoveryFailure
1523
- ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
1524
- : detailResult?.image_evidence?.ok === false
1525
- ? compactError({
1526
- code: detailResult.image_evidence.error_code,
2222
+ post_action: postActionResult,
2223
+ error: recoverableDetailError
2224
+ ? compactRecoverableDetailError(recoverableDetailError)
2225
+ : closeRecoveryFailure
2226
+ ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
2227
+ : detailResult?.image_evidence?.ok === false
2228
+ ? compactError({
2229
+ code: detailResult.image_evidence.error_code,
1527
2230
  message: detailResult.image_evidence.error
1528
2231
  }, "IMAGE_CAPTURE_FAILED")
1529
2232
  : null,
@@ -1558,70 +2261,80 @@ export async function runRecommendWorkflow({
1558
2261
  error: compactResult.error,
1559
2262
  post_action: postActionResult
1560
2263
  }
1561
- });
1562
- addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1563
-
1564
- if (closeRecoveryFailure) {
1565
- throw closeRecoveryFailure;
1566
- }
1567
-
1568
- if (postActionResult?.stop_run) {
1569
- listEndReason = postActionResult.reason || "post_action_stop";
1570
- break;
1571
- }
1572
-
1573
- if (effectiveHumanRestEnabled) {
1574
- const restStarted = Date.now();
1575
- const restResult = await humanRestController.takeBreakIfNeeded({
1576
- sleepFn: (ms) => runControl.sleep(ms)
1577
- });
1578
- const restElapsed = Date.now() - restStarted;
1579
- if (restResult.rested) {
1580
- recordHumanEvent({
1581
- kind: "rest",
1582
- pause_ms: restResult.pause_ms || restElapsed,
1583
- events: restResult.events || []
1584
- });
1585
- compactResult.human_rest = restResult;
1586
- addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1587
- compactResult.timings.total_ms = Date.now() - candidateStarted;
1588
- updateRecommendProgress({
1589
- human_rest_level: effectiveHumanBehavior.restLevel,
1590
- human_rest_last: restResult
1591
- });
1592
- }
1593
- }
1594
-
1595
- if (delayMs > 0) {
1596
- const sleepStarted = Date.now();
1597
- await runControl.sleep(delayMs);
1598
- addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1599
- compactResult.timings.total_ms = Date.now() - candidateStarted;
2264
+ });
2265
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
2266
+
2267
+ if (closeRecoveryFailure) {
2268
+ throw closeRecoveryFailure;
2269
+ }
2270
+
2271
+ if (postActionResult?.stop_run) {
2272
+ listEndReason = postActionResult.reason || "post_action_stop";
2273
+ break;
2274
+ }
2275
+
2276
+ if (effectiveHumanRestEnabled) {
2277
+ const restStarted = Date.now();
2278
+ const restResult = await humanRestController.takeBreakIfNeeded({
2279
+ sleepFn: (ms) => runControl.sleep(ms)
2280
+ });
2281
+ const restElapsed = Date.now() - restStarted;
2282
+ if (restResult.rested) {
2283
+ recordHumanEvent({
2284
+ kind: "rest",
2285
+ pause_ms: restResult.pause_ms || restElapsed,
2286
+ events: restResult.events || []
2287
+ });
2288
+ compactResult.human_rest = restResult;
2289
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
2290
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
2291
+ updateRecommendProgress({
2292
+ human_rest_level: effectiveHumanBehavior.restLevel,
2293
+ human_rest_last: restResult
2294
+ });
2295
+ }
2296
+ }
2297
+
2298
+ if (delayMs > 0) {
2299
+ const sleepStarted = Date.now();
2300
+ await runControl.sleep(delayMs);
2301
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
2302
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1600
2303
  }
1601
2304
  }
1602
2305
 
1603
2306
  runControl.setPhase("recommend:done");
1604
- return {
1605
- domain: "recommend",
1606
- target_url: targetUrl,
1607
- job_selection: compactJobSelection(jobSelection),
1608
- page_scope: compactPageScopeSelection(pageScopeSelection),
1609
- filter: compactFilterResult(filterResult),
1610
- card_count: cardNodeIds.length,
1611
- candidate_list: compactInfiniteListState(listState),
2307
+ return {
2308
+ domain: "recommend",
2309
+ target_url: targetUrl,
2310
+ job_selection: compactJobSelection(jobSelection),
2311
+ page_scope: compactPageScopeSelection(pageScopeSelection),
2312
+ current_city_only: compactCurrentCityOnlyResult(currentCityOnlyResult),
2313
+ filter: compactFilterResult(filterResult),
2314
+ card_count: cardNodeIds.length,
2315
+ candidate_list: compactInfiniteListState(listState),
1612
2316
  viewport_health: {
1613
2317
  stats: viewportGuard.getStats(),
1614
- events: viewportGuard.getEvents()
1615
- },
1616
- human_behavior: effectiveHumanBehavior,
1617
- human_rest: humanRestController.getState(),
1618
- last_human_event: lastHumanEvent,
1619
- list_end_reason: listEndReason || null,
2318
+ events: viewportGuard.getEvents()
2319
+ },
2320
+ human_behavior: effectiveHumanBehavior,
2321
+ human_rest: humanRestController.getState(),
2322
+ last_human_event: lastHumanEvent,
2323
+ list_end_reason: listEndReason || null,
1620
2324
  refresh_rounds: refreshRounds,
1621
- refresh_attempts: refreshAttempts,
1622
- context_recoveries: contextRecoveryAttempts,
1623
- ...countRecommendResultStatuses(results, { greetCount }),
1624
- results
2325
+ refresh_attempts: refreshAttempts,
2326
+ context_recoveries: contextRecoveryAttempts,
2327
+ debug_boundary: debugBoundary.getState(),
2328
+ list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
2329
+ list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
2330
+ list_read_stale_recoveries: listReadStaleRecoveries,
2331
+ last_list_read_recovery_mode: lastListReadRecoveryMode,
2332
+ last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
2333
+ list_read_stale_diagnostics: listReadStaleDiagnostics.slice(-12),
2334
+ ...countRecommendResultStatuses(results, { greetCount }),
2335
+ transient_recovered: countRecommendResultStatuses(results, { greetCount }).transient_recovered
2336
+ + listReadStaleRecoveries,
2337
+ results
1625
2338
  };
1626
2339
  }
1627
2340
 
@@ -1667,48 +2380,59 @@ export function createRecommendRunService({
1667
2380
  actionIntervalMs = 500,
1668
2381
  actionAfterClickDelayMs = 900,
1669
2382
  screeningMode = "llm",
1670
- llmConfig = null,
1671
- llmTimeoutMs = 120000,
1672
- llmImageLimit = 8,
1673
- llmImageDetail = "high",
1674
- imageOutputDir = "",
1675
- humanRestEnabled = false,
2383
+ llmConfig = null,
2384
+ llmTimeoutMs = 120000,
2385
+ llmImageLimit = 8,
2386
+ llmImageDetail = "high",
2387
+ imageOutputDir = "",
2388
+ humanRestEnabled = false,
1676
2389
  humanBehavior = null,
1677
2390
  skipRecentColleagueContacted = true,
1678
2391
  colleagueContactWindowDays = 14,
2392
+ debugTestMode = undefined,
2393
+ debugForceListEndAfterProcessed = undefined,
2394
+ debugForceContextRecoveryAfterProcessed = undefined,
2395
+ debugForceCdpReconnectAfterProcessed = undefined,
1679
2396
  name = "recommend-domain-run"
1680
2397
  } = {}) {
1681
- if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1682
- const normalizedFilter = normalizeFilter(filter);
1683
- const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1684
- const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1685
- const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1686
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1687
- const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
1688
- const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
1689
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1690
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1691
- });
2398
+ if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
2399
+ const normalizedFilter = normalizeFilter(filter);
2400
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
2401
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
2402
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
2403
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
2404
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
2405
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
2406
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2407
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2408
+ });
1692
2409
  const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1693
2410
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1694
- const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1695
- return manager.startRun({
2411
+ const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
2412
+ const debugBoundaryOptions = normalizeRecommendDebugBoundaryOptions({
2413
+ debugTestMode: debugTestMode === true,
2414
+ debugForceListEndAfterProcessed: debugForceListEndAfterProcessed ?? null,
2415
+ debugForceContextRecoveryAfterProcessed: debugForceContextRecoveryAfterProcessed ?? null,
2416
+ debugForceCdpReconnectAfterProcessed: debugForceCdpReconnectAfterProcessed ?? null
2417
+ });
2418
+ return manager.startRun({
1696
2419
  runId,
1697
2420
  name,
1698
2421
  pid,
1699
2422
  context: {
1700
2423
  domain: "recommend",
1701
2424
  target_url: targetUrl,
1702
- criteria_present: Boolean(criteria),
1703
- job_label: jobLabel || "",
1704
- requested_page_scope: requestedPageScope,
1705
- fallback_page_scope: normalizedFallbackPageScope,
1706
- filter: normalizedFilter,
1707
- max_candidates: maxCandidates,
1708
- max_candidates_semantics: "passed_candidates",
1709
- detail_limit: normalizedDetailLimit,
1710
- close_detail: closeDetail,
1711
- cv_acquisition_mode: cvAcquisitionMode,
2425
+ criteria_present: Boolean(criteria),
2426
+ job_label: jobLabel || "",
2427
+ requested_page_scope: requestedPageScope,
2428
+ fallback_page_scope: normalizedFallbackPageScope,
2429
+ filter: normalizedFilter,
2430
+ current_city_only_requested: normalizedFilter.currentCityOnly,
2431
+ max_candidates: maxCandidates,
2432
+ max_candidates_semantics: "passed_candidates",
2433
+ detail_limit: normalizedDetailLimit,
2434
+ close_detail: closeDetail,
2435
+ cv_acquisition_mode: cvAcquisitionMode,
1712
2436
  max_image_pages: maxImagePages,
1713
2437
  image_wheel_delta_y: imageWheelDeltaY,
1714
2438
  list_max_scrolls: listMaxScrolls,
@@ -1726,45 +2450,55 @@ export function createRecommendRunService({
1726
2450
  action_timeout_ms: actionTimeoutMs,
1727
2451
  screening_mode: normalizedScreeningMode,
1728
2452
  llm_configured: Boolean(llmConfig),
1729
- llm_timeout_ms: llmTimeoutMs,
1730
- llm_image_limit: llmImageLimit,
1731
- llm_image_detail: llmImageDetail,
1732
- image_output_dir: imageOutputDir || "",
1733
- skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
1734
- colleague_contact_window_days: normalizedColleagueContactWindowDays,
1735
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1736
- human_behavior_profile: effectiveHumanBehavior.profile,
2453
+ llm_timeout_ms: llmTimeoutMs,
2454
+ llm_image_limit: llmImageLimit,
2455
+ llm_image_detail: llmImageDetail,
2456
+ image_output_dir: imageOutputDir || "",
2457
+ skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
2458
+ colleague_contact_window_days: normalizedColleagueContactWindowDays,
2459
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
2460
+ human_behavior_profile: effectiveHumanBehavior.profile,
1737
2461
  human_behavior: effectiveHumanBehavior,
1738
2462
  human_rest_level: effectiveHumanBehavior.restLevel,
1739
- human_rest_enabled: effectiveHumanRestEnabled
1740
- },
1741
- progress: {
2463
+ human_rest_enabled: effectiveHumanRestEnabled,
2464
+ debug_test_mode: debugBoundaryOptions.debugTestMode,
2465
+ debug_boundary_mode: debugBoundaryOptions.mode,
2466
+ debug_boundary_threshold: debugBoundaryOptions.threshold
2467
+ },
2468
+ progress: {
1742
2469
  card_count: 0,
1743
2470
  target_count: candidateLimit,
1744
2471
  target_count_semantics: "passed_candidates",
1745
2472
  processed: 0,
1746
2473
  screened: 0,
1747
2474
  detail_opened: 0,
1748
- llm_screened: 0,
1749
- passed: 0,
1750
- skipped: 0,
1751
- greet_count: 0,
2475
+ llm_screened: 0,
2476
+ passed: 0,
2477
+ skipped: 0,
2478
+ greet_count: 0,
1752
2479
  post_action_clicked: 0,
1753
- image_capture_failed: 0,
1754
- detail_open_failed: 0,
2480
+ image_capture_failed: 0,
2481
+ detail_open_failed: 0,
1755
2482
  transient_recovered: 0,
1756
2483
  colleague_contact_checked: 0,
1757
2484
  recent_colleague_contact_skipped: 0,
1758
2485
  colleague_contact_panel_missing: 0,
1759
2486
  context_recoveries: 0,
2487
+ current_city_only_requested: normalizedFilter.currentCityOnly,
2488
+ current_city_only_effective: null,
2489
+ current_city_only_unavailable: false,
1760
2490
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1761
2491
  human_behavior_profile: effectiveHumanBehavior.profile,
1762
2492
  human_rest_level: effectiveHumanBehavior.restLevel,
1763
2493
  human_rest_enabled: effectiveHumanRestEnabled,
1764
2494
  human_rest_count: 0,
1765
2495
  human_rest_ms: 0,
1766
- last_human_event: null
1767
- },
2496
+ last_human_event: null,
2497
+ debug_boundary_mode: debugBoundaryOptions.mode,
2498
+ debug_boundary_threshold: debugBoundaryOptions.threshold,
2499
+ debug_boundary_triggered: false,
2500
+ debug_boundary_trigger_count: 0
2501
+ },
1768
2502
  checkpoint: {},
1769
2503
  task: (runControl) => workflow({
1770
2504
  client,
@@ -1799,17 +2533,21 @@ export function createRecommendRunService({
1799
2533
  actionAfterClickDelayMs,
1800
2534
  screeningMode: normalizedScreeningMode,
1801
2535
  llmConfig,
1802
- llmTimeoutMs,
1803
- llmImageLimit,
1804
- llmImageDetail,
1805
- imageOutputDir,
1806
- humanRestEnabled: effectiveHumanRestEnabled,
2536
+ llmTimeoutMs,
2537
+ llmImageLimit,
2538
+ llmImageDetail,
2539
+ imageOutputDir,
2540
+ humanRestEnabled: effectiveHumanRestEnabled,
1807
2541
  humanBehavior: effectiveHumanBehavior,
1808
2542
  skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
1809
- colleagueContactWindowDays: normalizedColleagueContactWindowDays
2543
+ colleagueContactWindowDays: normalizedColleagueContactWindowDays,
2544
+ debugTestMode: debugBoundaryOptions.debugTestMode,
2545
+ debugForceListEndAfterProcessed: debugBoundaryOptions.debugForceListEndAfterProcessed,
2546
+ debugForceContextRecoveryAfterProcessed: debugBoundaryOptions.debugForceContextRecoveryAfterProcessed,
2547
+ debugForceCdpReconnectAfterProcessed: debugBoundaryOptions.debugForceCdpReconnectAfterProcessed
1810
2548
  }, runControl)
1811
- });
1812
- }
2549
+ });
2550
+ }
1813
2551
 
1814
2552
  return {
1815
2553
  startRecommendRun,