@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 +8 -8
  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 +2419 -2415
  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 +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  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 +650 -650
  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 +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  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 +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -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";
71
61
  import {
72
- normalizeRecommendPageScope,
73
- selectRecommendPageScope
74
- } from "./scopes.js";
75
- import { inspectRecentColleagueContact } from "./colleague-contact.js";
62
+ readRecommendCardCandidate,
63
+ waitForRecommendCardNodeIds
64
+ } from "./cards.js";
65
+ import { selectAndConfirmFirstSafeFilter } from "./filters.js";
66
+ import { ensureRecommendCurrentCityOnly } from "./location.js";
67
+ import {
68
+ applyRecommendFilterEnvelopeStages,
69
+ buildRecommendFilterSelectionOptions,
70
+ refreshRecommendListAtEnd
71
+ } from "./refresh.js";
72
+ import { selectRecommendJob } from "./jobs.js";
73
+ import {
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,
@@ -104,26 +106,46 @@ function isRefreshableListStall(reason = "") {
104
106
  function normalizeFilter(filter = {}) {
105
107
  const filterGroups = Array.isArray(filter.filterGroups)
106
108
  ? 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),
109
+ : Array.isArray(filter.groups)
110
+ ? filter.groups
111
+ : [];
112
+ return {
113
+ enabled: filter.enabled !== false,
114
+ currentCityOnly: filter.currentCityOnly === true || filter.current_city_only === true,
115
+ group: String(filter.group || ""),
116
+ labels: normalizeLabels(filter.labels || filter.filterLabels || []),
117
+ selectAllLabels: Boolean(filter.selectAllLabels),
118
+ allowUnlimited: filter.allowUnlimited === true,
119
+ verifySticky: filter.verifySticky === true,
120
+ filterGroups: filterGroups.map((group) => ({
121
+ group: String(group?.group || ""),
122
+ labels: normalizeLabels(group?.labels || group?.filterLabels || []),
123
+ selectAllLabels: group?.selectAllLabels !== false,
124
+ allowUnlimited: group?.allowUnlimited === true,
125
+ verifySticky: group?.verifySticky === true
126
+ })).filter((group) => group.group || group.labels.length)
127
+ };
128
+ }
129
+
130
+ export function compactFilterResult(filterResult) {
131
+ if (!filterResult) return null;
132
+ return {
133
+ opened_panel: Boolean(filterResult.opened_panel),
134
+ requested_groups: (filterResult.requested_groups || []).map((group) => ({
135
+ group: group.group,
136
+ labels: group.labels || [],
137
+ select_all_labels: group.select_all_labels !== false,
138
+ allow_unlimited: Boolean(group.allow_unlimited),
139
+ verify_sticky: Boolean(group.verify_sticky)
140
+ })),
141
+ effective_groups: (filterResult.sticky_verification?.groups || []).map((group) => ({
142
+ group: group.group,
143
+ requested_labels: group.requested_labels || [],
144
+ active_labels: group.active_labels || [],
145
+ verified: Boolean(group.verified),
146
+ unavailable: Boolean(group.unavailable),
147
+ reason: group.reason || null
148
+ })),
127
149
  selected_option: filterResult.selected_option
128
150
  ? {
129
151
  group: filterResult.selected_option.group,
@@ -133,49 +155,98 @@ function compactFilterResult(filterResult) {
133
155
  }
134
156
  : null,
135
157
  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
158
+ group: option.group,
159
+ label: option.label,
160
+ was_active: Boolean(option.was_active),
161
+ clicked: option.clicked !== false
162
+ })),
163
+ unavailable: Boolean(filterResult.unavailable),
164
+ unavailable_groups: filterResult.unavailable_groups || [],
165
+ confirmed: Boolean(filterResult.confirmed),
166
+ sticky_verification: filterResult.sticky_verification || null,
167
+ attempts: {
168
+ initial_close: filterResult.initial_close_attempts || [],
169
+ open: (filterResult.open_attempts || []).map((attempt) => ({
170
+ selector: attempt.selector || null,
171
+ node_id: attempt.node_id || null,
172
+ click_target: attempt.click_target || null
173
+ })),
174
+ confirmation: (filterResult.confirm_attempts || []).map((attempt) => ({
175
+ node_id: attempt.node_id || null,
176
+ label: attempt.label || null,
177
+ clicked: Boolean(attempt.clicked),
178
+ errors: (attempt.errors || []).map((error) => ({
179
+ node_id: error.node_id || null,
180
+ message: error.message || String(error)
181
+ }))
182
+ }))
183
+ },
184
+ before_counts: filterResult.before_counts,
185
+ after_confirm_counts: filterResult.after_confirm_counts
186
+ };
187
+ }
188
+
189
+ function compactCurrentCityOnlyResult(result) {
190
+ if (!result) return null;
191
+ return {
192
+ requested: Boolean(result.requested),
193
+ effective: typeof result.effective === "boolean" ? result.effective : null,
194
+ available: result.available !== false,
195
+ unavailable: Boolean(result.unavailable),
196
+ reason: result.reason || null,
197
+ clicked: Boolean(result.clicked),
198
+ current_city_label: result.current_city_label || null,
199
+ before: result.before || null,
200
+ after_toggle: result.after_toggle || null,
201
+ confirmation: result.confirmation || null,
202
+ sticky_verification: result.sticky_verification
163
203
  ? {
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
204
+ verified: Boolean(result.sticky_verification.verified),
205
+ expected: Boolean(result.sticky_verification.expected),
206
+ actual: typeof result.sticky_verification.actual === "boolean"
207
+ ? result.sticky_verification.actual
208
+ : null,
209
+ state_source: result.sticky_verification.state_source || null,
210
+ close_confirmation: result.sticky_verification.close_confirmation || null
176
211
  }
177
212
  : null,
178
- options: (jobSelection.options || []).map((option) => ({
213
+ attempts: Array.isArray(result.attempts) ? result.attempts : [],
214
+ evidence: result.evidence || null
215
+ };
216
+ }
217
+
218
+ function compactJobSelection(jobSelection) {
219
+ if (!jobSelection) return null;
220
+ return {
221
+ requested: jobSelection.requested || "",
222
+ selected: Boolean(jobSelection.selected),
223
+ already_current: Boolean(jobSelection.already_current),
224
+ reason: jobSelection.reason || null,
225
+ selected_option: jobSelection.selected_option || null,
226
+ menu_close: jobSelection.menu_close
227
+ ? {
228
+ ok: Boolean(jobSelection.menu_close.ok),
229
+ closed: Boolean(jobSelection.menu_close.closed),
230
+ reason: jobSelection.menu_close.reason || ""
231
+ }
232
+ : null,
233
+ sticky_verification: jobSelection.sticky_verification
234
+ ? {
235
+ verified: Boolean(jobSelection.sticky_verification.verified),
236
+ current_label: jobSelection.sticky_verification.current_label_without_salary
237
+ || jobSelection.sticky_verification.current_label
238
+ || "",
239
+ visible_option_count: jobSelection.sticky_verification.visible_option_count || 0,
240
+ menu_close: jobSelection.sticky_verification.menu_close
241
+ ? {
242
+ ok: Boolean(jobSelection.sticky_verification.menu_close.ok),
243
+ closed: Boolean(jobSelection.sticky_verification.menu_close.closed),
244
+ reason: jobSelection.sticky_verification.menu_close.reason || ""
245
+ }
246
+ : null
247
+ }
248
+ : null,
249
+ options: (jobSelection.options || []).map((option) => ({
179
250
  label: option.label,
180
251
  label_without_salary: option.label_without_salary,
181
252
  current: Boolean(option.current),
@@ -225,20 +296,20 @@ function compactCandidate(candidate) {
225
296
  };
226
297
  }
227
298
 
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,
299
+ function compactDetail(detailResult) {
300
+ if (!detailResult) return null;
301
+ return {
302
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
303
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
233
304
  network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
234
305
  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
- }
306
+ cv_acquisition: detailResult.cv_acquisition || null,
307
+ colleague_contact: detailResult.colleague_contact || null,
308
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
309
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
310
+ close_result: detailResult.close_result
311
+ };
312
+ }
242
313
 
243
314
  function normalizeScreeningMode(value) {
244
315
  const normalized = String(value || "llm").trim().toLowerCase();
@@ -287,22 +358,22 @@ async function runRecommendPostAction({
287
358
  reason: ""
288
359
  };
289
360
 
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;
361
+ if (!screening?.passed) {
362
+ result.reason = "screening_not_passed";
363
+ return result;
364
+ }
365
+ if (plan.effective === "none") {
366
+ result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
367
+ return result;
368
+ }
369
+
370
+ const summary = actionDiscovery?.summary || {};
371
+ const control = summary.greet;
372
+ if (!control?.found) {
373
+ result.reason = `${plan.effective}_control_not_found`;
374
+ return result;
375
+ }
376
+ result.control = control;
306
377
 
307
378
  if (plan.effective === "greet" && control.continue_chat) {
308
379
  result.reason = "already_connected_continue_chat";
@@ -319,10 +390,10 @@ async function runRecommendPostAction({
319
390
  result.reason = "greet_control_not_available";
320
391
  return result;
321
392
  }
322
- if (control.disabled) {
323
- result.reason = `${plan.effective}_control_disabled`;
324
- return result;
325
- }
393
+ if (control.disabled) {
394
+ result.reason = `${plan.effective}_control_disabled`;
395
+ return result;
396
+ }
326
397
  if (!executePostAction) {
327
398
  result.reason = "dry_run_post_action";
328
399
  result.would_click = true;
@@ -357,18 +428,18 @@ async function runRecommendPostAction({
357
428
  timeoutMs: 2500,
358
429
  intervalMs: 300,
359
430
  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) {
431
+ });
432
+ const afterSummary = afterDiscovery?.summary || {};
433
+ const afterControl = afterSummary.greet;
434
+ result.action_discovery_after = compactActionDiscovery(afterDiscovery);
435
+ result.control_after = afterControl || null;
436
+ if (plan.effective === "greet") {
437
+ result.verified_after_click = Boolean(
438
+ afterControl?.continue_chat
439
+ || String(afterControl?.label || "").includes("继续沟通")
440
+ );
441
+ }
442
+ } catch (error) {
372
443
  result.verify_error = {
373
444
  message: error?.message || String(error)
374
445
  };
@@ -383,176 +454,195 @@ function compactRefreshAttempt(refreshAttempt) {
383
454
  method: refreshAttempt.method || "",
384
455
  reason: refreshAttempt.reason || null,
385
456
  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) => ({
457
+ forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
458
+ target_url: refreshAttempt.target_url || null,
459
+ card_count: refreshAttempt.card_count || 0,
460
+ elapsed_ms: refreshAttempt.elapsed_ms || 0,
461
+ recovery_settle: refreshAttempt.recovery_settle
462
+ ? {
463
+ ok: Boolean(refreshAttempt.recovery_settle.ok),
464
+ status: refreshAttempt.recovery_settle.status || "",
465
+ reason: refreshAttempt.recovery_settle.reason || "",
466
+ elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
467
+ }
468
+ : null,
469
+ attempts: (refreshAttempt.attempts || []).map((attempt) => ({
399
470
  ok: Boolean(attempt.ok),
400
471
  method: attempt.method || "",
401
472
  reason: attempt.reason || null,
402
- error: attempt.error || null,
403
- label: attempt.label || null,
473
+ error: attempt.error || null,
474
+ label: attempt.label || null,
404
475
  before_card_count: attempt.before_card_count || 0,
405
476
  after_card_count: attempt.after_card_count || 0,
406
477
  card_count: attempt.card_count || 0,
407
- elapsed_ms: attempt.elapsed_ms || 0
478
+ elapsed_ms: attempt.elapsed_ms || 0,
479
+ current_city_only: compactCurrentCityOnlyResult(attempt.current_city_only),
480
+ current_city_only_attempts: (attempt.current_city_only_attempts || []).map((cityAttempt) => ({
481
+ ok: Boolean(cityAttempt.ok),
482
+ method: cityAttempt.method || "current_city_only_reapply",
483
+ reason: cityAttempt.reason || null,
484
+ error: cityAttempt.error || null,
485
+ attempt: cityAttempt.attempt || 0,
486
+ result: compactCurrentCityOnlyResult(cityAttempt.result)
487
+ })),
488
+ filter: compactFilterResult(attempt.filter)
408
489
  })),
409
- filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
490
+ current_city_only_attempts: (refreshAttempt.current_city_only_attempts || []).map((attempt) => ({
410
491
  ok: Boolean(attempt.ok),
411
- method: attempt.method || "filter_reapply",
492
+ method: attempt.method || "current_city_only_reapply",
412
493
  reason: attempt.reason || null,
413
494
  error: attempt.error || null,
414
- attempt: attempt.attempt || 0
495
+ attempt: attempt.attempt || 0,
496
+ result: compactCurrentCityOnlyResult(attempt.result)
415
497
  })),
416
- job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
498
+ filter_reapply_attempts: (refreshAttempt.filter_reapply_attempts || []).map((attempt) => ({
417
499
  ok: Boolean(attempt.ok),
418
- method: attempt.method || "job_select",
500
+ method: attempt.method || "filter_reapply",
419
501
  reason: attempt.reason || null,
420
- error: attempt.error || null,
421
- attempt: attempt.attempt || 0,
422
- iframe_document_node_id: attempt.iframe_document_node_id || 0,
502
+ error: attempt.error || null,
503
+ attempt: attempt.attempt || 0
504
+ })),
505
+ job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
506
+ ok: Boolean(attempt.ok),
507
+ method: attempt.method || "job_select",
508
+ reason: attempt.reason || null,
509
+ error: attempt.error || null,
510
+ attempt: attempt.attempt || 0,
511
+ iframe_document_node_id: attempt.iframe_document_node_id || 0,
423
512
  selected: Boolean(attempt.selected),
424
513
  selection_reason: attempt.selection_reason || null
425
514
  })),
426
515
  job_selection: compactJobSelection(refreshAttempt.job_selection),
427
516
  page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
517
+ current_city_only: compactCurrentCityOnlyResult(refreshAttempt.current_city_only),
428
518
  filter: compactFilterResult(refreshAttempt.filter)
429
519
  };
430
520
  }
431
-
521
+
432
522
  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,
523
+ greetCount = 0
524
+ } = {}) {
525
+ return {
526
+ processed: results.length,
527
+ screened: results.length,
528
+ detail_opened: results.filter((item) => item.detail).length,
529
+ passed: results.filter((item) => item.screening?.passed).length,
530
+ skipped: results.filter((item) => item.screening?.passed === false).length,
531
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
532
+ greet_count: greetCount,
533
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
534
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
445
535
  detail_open_failed: results.filter((item) => (
446
536
  item.error?.code === "DETAIL_STALE_NODE"
447
537
  || 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
- }
538
+ )).length,
539
+ transient_recovered: results.filter((item) => (
540
+ item.error?.code === "DETAIL_STALE_NODE"
541
+ || item.error?.code === "DETAIL_OPEN_FAILED"
542
+ || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
543
+ || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
544
+ || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
545
+ )).length,
546
+ colleague_contact_checked: results.filter((item) => item.detail?.colleague_contact?.checked).length,
547
+ recent_colleague_contact_skipped: results.filter((item) => (
548
+ item.screening?.status === "skip"
549
+ && item.screening?.reasons?.includes("skipped_recent_colleague_contact")
550
+ )).length,
551
+ colleague_contact_panel_missing: results.filter((item) => (
552
+ item.detail?.colleague_contact?.reason === "panel_missing"
553
+ )).length
554
+ };
555
+ }
466
556
 
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
- }
498
- if (error.refresh_attempt) {
499
- result.refresh_attempt = error.refresh_attempt;
500
- }
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;
515
- }
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
- }
557
+ function countPassedResults(results = []) {
558
+ return countRecommendResultStatuses(results).passed;
559
+ }
525
560
 
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;
531
- }
532
-
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;
538
- return error;
539
- }
540
-
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;
552
- error.target_count = targetCount;
553
- error.passed_count = passedCount;
554
- return error;
555
- }
561
+ function compactCloseResult(closeResult) {
562
+ if (!closeResult) return null;
563
+ const result = {
564
+ closed: Boolean(closeResult.closed),
565
+ reason: closeResult.reason || null,
566
+ probe: closeResult.probe || null,
567
+ attempts: closeResult.attempts || [],
568
+ verification: closeResult.verification || null
569
+ };
570
+ if (closeResult.already_closed !== undefined) {
571
+ result.already_closed = Boolean(closeResult.already_closed);
572
+ }
573
+ return result;
574
+ }
575
+
576
+ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
577
+ if (!error) return null;
578
+ const result = {
579
+ code: error.code || fallbackCode,
580
+ message: error.message || String(error)
581
+ };
582
+ if (error.close_result) {
583
+ result.close_result = compactCloseResult(error.close_result);
584
+ }
585
+ if (error.phase) {
586
+ result.phase = error.phase;
587
+ }
588
+ if (error.refresh_attempt) {
589
+ result.refresh_attempt = error.refresh_attempt;
590
+ }
591
+ if (error.list_end_reason) {
592
+ result.list_end_reason = error.list_end_reason;
593
+ }
594
+ if (error.target_count != null) {
595
+ result.target_count = error.target_count;
596
+ }
597
+ if (error.passed_count != null) {
598
+ result.passed_count = error.passed_count;
599
+ }
600
+ if (Array.isArray(error.recommend_detail_open_attempts)) {
601
+ result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
602
+ }
603
+ if (Array.isArray(error.click_attempts)) {
604
+ result.click_attempts = error.click_attempts;
605
+ }
606
+ if (error.avatar_preview) {
607
+ result.avatar_preview = {
608
+ open: Boolean(error.avatar_preview.open),
609
+ selector: error.avatar_preview.preview?.selector || null,
610
+ rect: error.avatar_preview.preview?.rect || null
611
+ };
612
+ }
613
+ return result;
614
+ }
615
+
616
+ function createRecommendCloseFailureError(closeResult) {
617
+ const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
618
+ error.code = "DETAIL_CLOSE_FAILED";
619
+ error.close_result = closeResult || null;
620
+ return error;
621
+ }
622
+
623
+ function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "") {
624
+ const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
625
+ error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
626
+ error.close_result = closeResult || null;
627
+ error.phase = phase || null;
628
+ return error;
629
+ }
630
+
631
+ function createRecommendRefreshFailureError(refreshAttempt, {
632
+ listEndReason = "",
633
+ targetCount = 0,
634
+ passedCount = 0
635
+ } = {}) {
636
+ const reason = refreshAttempt?.reason || "refresh_failed";
637
+ const detail = refreshAttempt?.error ? `: ${refreshAttempt.error}` : "";
638
+ const error = new Error(`Recommend refresh failed before target was reached (${reason}${detail})`);
639
+ error.code = "RECOMMEND_END_REFRESH_FAILED";
640
+ error.refresh_attempt = refreshAttempt || null;
641
+ error.list_end_reason = listEndReason || null;
642
+ error.target_count = targetCount;
643
+ error.passed_count = passedCount;
644
+ return error;
645
+ }
556
646
 
557
647
  export function isRecoverableImageCaptureError(error) {
558
648
  const code = String(error?.code || "");
@@ -615,41 +705,41 @@ function createImageCaptureFailureScreening(candidate, error) {
615
705
  };
616
706
  }
617
707
 
618
- export function isRecoverableRecommendDetailError(error) {
619
- return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
620
- }
708
+ export function isRecoverableRecommendDetailError(error) {
709
+ return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
710
+ }
621
711
 
622
712
  function compactRecoverableDetailError(error) {
623
713
  return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
624
714
  }
625
715
 
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"],
716
+ function createRecoverableDetailFailureScreening(candidate, error) {
717
+ return {
718
+ status: "fail",
719
+ passed: false,
720
+ score: 0,
721
+ reasons: isStaleRecommendNodeError(error)
722
+ ? ["detail_open_failed", "stale_node"]
723
+ : isRecommendDetailOpenMissError(error)
724
+ ? ["detail_open_failed", "detail_open_miss"]
725
+ : ["detail_open_failed"],
636
726
  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
- }
727
+ candidate
728
+ };
729
+ }
730
+
731
+ function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
732
+ const matched = colleagueContact?.matched_row || null;
733
+ return {
734
+ status: "skip",
735
+ passed: false,
736
+ score: 0,
737
+ reasons: ["skipped_recent_colleague_contact"],
738
+ reason: matched?.text || "Candidate has recent colleague contact history",
739
+ matched_colleague_contact: matched,
740
+ candidate
741
+ };
742
+ }
653
743
 
654
744
  export async function runRecommendWorkflow({
655
745
  client,
@@ -681,47 +771,47 @@ export async function runRecommendWorkflow({
681
771
  executePostAction = true,
682
772
  actionTimeoutMs = 8000,
683
773
  actionIntervalMs = 500,
684
- actionAfterClickDelayMs = 900,
685
- screeningMode = "llm",
686
- llmConfig = null,
687
- llmTimeoutMs = 120000,
688
- llmImageLimit = 8,
689
- llmImageDetail = "high",
690
- imageOutputDir = "",
691
- humanRestEnabled = false,
692
- humanBehavior = null,
693
- skipRecentColleagueContacted = true,
694
- colleagueContactWindowDays = 14
695
- } = {}, runControl) {
696
- 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
707
- });
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);
774
+ actionAfterClickDelayMs = 900,
775
+ screeningMode = "llm",
776
+ llmConfig = null,
777
+ llmTimeoutMs = 120000,
778
+ llmImageLimit = 8,
779
+ llmImageDetail = "high",
780
+ imageOutputDir = "",
781
+ humanRestEnabled = false,
782
+ humanBehavior = null,
783
+ skipRecentColleagueContacted = true,
784
+ colleagueContactWindowDays = 14
785
+ } = {}, runControl) {
786
+ if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
787
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
788
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
789
+ });
790
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
791
+ configureHumanInteraction(client, {
792
+ enabled: effectiveHumanBehavior.enabled,
793
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
794
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
795
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
796
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
797
+ });
798
+ const humanRestController = createHumanRestController({
799
+ enabled: effectiveHumanRestEnabled,
800
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
801
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
802
+ restLevel: effectiveHumanBehavior.restLevel
803
+ });
804
+ const normalizedFilter = normalizeFilter(filter);
715
805
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
716
806
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
717
807
  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);
808
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
809
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
810
+ const postActionEnabled = normalizedPostAction !== "none";
811
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
812
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
813
+ const colleagueContactReferenceDate = new Date();
814
+ const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
725
815
  const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
726
816
  const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
727
817
  const networkRecorder = effectiveDetailLimit > 0
@@ -747,59 +837,60 @@ export async function runRecommendWorkflow({
747
837
  const results = [];
748
838
  const refreshAttempts = [];
749
839
  let refreshRounds = 0;
750
- let contextRecoveryAttempts = 0;
751
- let greetCount = 0;
840
+ let contextRecoveryAttempts = 0;
841
+ let greetCount = 0;
752
842
  const candidateRecoveryCounts = new Map();
753
843
  let jobSelection = null;
754
844
  let pageScopeSelection = null;
845
+ let currentCityOnlyResult = null;
755
846
  let filterResult = null;
756
847
  let rootState = null;
757
848
  let cardNodeIds = [];
758
849
  let listEndReason = "";
759
850
  let lastHumanEvent = null;
760
- const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
851
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
761
852
  rootNodeId: rootState?.iframe?.documentNodeId,
762
853
  containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
763
854
  itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
764
855
  itemSelectors: [RECOMMEND_CARD_SELECTOR],
765
856
  viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
766
857
  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
-
795
- function updateRecommendProgress(extra = {}) {
796
- const counts = countRecommendResultStatuses(results, { greetCount });
797
- const listSnapshot = compactInfiniteListState(listState);
798
- const humanRestState = humanRestController.getState();
799
- runControl.updateProgress({
800
- card_count: cardNodeIds.length,
801
- target_count: targetPassCount,
802
- target_count_semantics: "passed_candidates",
858
+ }));
859
+
860
+ function recordHumanEvent(event = null) {
861
+ if (!event) return lastHumanEvent;
862
+ lastHumanEvent = {
863
+ at: new Date().toISOString(),
864
+ ...event
865
+ };
866
+ return lastHumanEvent;
867
+ }
868
+
869
+ async function maybeHumanActionCooldown(phase, timings = {}) {
870
+ if (!effectiveHumanBehavior.actionCooldown) return null;
871
+ const pauseMs = humanDelay(280, 90, {
872
+ minMs: 80,
873
+ maxMs: 720
874
+ });
875
+ if (pauseMs > 0) {
876
+ await runControl.sleep(pauseMs);
877
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
878
+ }
879
+ return recordHumanEvent({
880
+ kind: "action_cooldown",
881
+ phase,
882
+ pause_ms: pauseMs
883
+ });
884
+ }
885
+
886
+ function updateRecommendProgress(extra = {}) {
887
+ const counts = countRecommendResultStatuses(results, { greetCount });
888
+ const listSnapshot = compactInfiniteListState(listState);
889
+ const humanRestState = humanRestController.getState();
890
+ runControl.updateProgress({
891
+ card_count: cardNodeIds.length,
892
+ target_count: targetPassCount,
893
+ target_count_semantics: "passed_candidates",
803
894
  ...counts,
804
895
  screening_mode: normalizedScreeningMode,
805
896
  unique_seen: listSnapshot.seen_count,
@@ -807,25 +898,28 @@ export async function runRecommendWorkflow({
807
898
  refresh_rounds: refreshRounds,
808
899
  refresh_attempts: refreshAttempts.length,
809
900
  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,
901
+ list_end_reason: listEndReason || null,
902
+ viewport_checks: viewportGuard.getStats().checks,
903
+ viewport_recoveries: viewportGuard.getStats().recoveries,
904
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
814
905
  human_behavior_profile: effectiveHumanBehavior.profile,
815
906
  human_rest_level: effectiveHumanBehavior.restLevel,
816
907
  human_rest_enabled: effectiveHumanRestEnabled,
817
908
  skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
818
909
  colleague_contact_window_days: normalizedColleagueContactWindowDays,
910
+ current_city_only_requested: normalizedFilter.currentCityOnly,
911
+ current_city_only_effective: currentCityOnlyResult?.effective ?? null,
912
+ current_city_only_unavailable: Boolean(currentCityOnlyResult?.unavailable),
819
913
  human_rest_count: humanRestState.rest_count,
820
914
  human_rest_ms: humanRestState.total_rest_ms,
821
915
  last_human_event: lastHumanEvent,
822
916
  ...extra
823
917
  });
824
- }
918
+ }
825
919
 
826
- function checkpointInProgressCandidate({
827
- index = results.length,
828
- candidateKey = "",
920
+ function checkpointInProgressCandidate({
921
+ index = results.length,
922
+ candidateKey = "",
829
923
  cardNodeId = null,
830
924
  detailStep = "",
831
925
  error = null
@@ -839,58 +933,59 @@ export async function runRecommendWorkflow({
839
933
  counters: countRecommendResultStatuses(results, { greetCount }),
840
934
  error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
841
935
  },
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, {
936
+ candidate_list: compactInfiniteListState(listState)
937
+ });
938
+ }
939
+
940
+ async function closeRecommendBlockingPanelsForRun(phase = "cleanup") {
941
+ const result = await closeRecommendBlockingPanels(client, {
942
+ attemptsLimit: 2,
943
+ rootState
944
+ });
945
+ if (!result?.closed) {
946
+ throw createRecommendBlockingPanelCloseFailureError(result, phase);
947
+ }
948
+ return result;
949
+ }
950
+
951
+ async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
858
952
  forceRecentNotView = true
859
953
  } = {}) {
860
- await runControl.waitIfPaused();
861
- runControl.throwIfCanceled();
862
- const started = Date.now();
863
- runControl.setPhase("recommend:recover-context");
864
- contextRecoveryAttempts += 1;
954
+ await runControl.waitIfPaused();
955
+ runControl.throwIfCanceled();
956
+ const started = Date.now();
957
+ runControl.setPhase("recommend:recover-context");
958
+ contextRecoveryAttempts += 1;
959
+ const effectiveForceRecentNotView = normalizedFilter.enabled && forceRecentNotView;
865
960
  const refreshResult = await refreshRecommendListAtEnd(client, {
866
961
  rootState,
867
962
  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,
963
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
964
+ fallbackPageScope: normalizedFallbackPageScope,
965
+ filter: normalizedFilter,
966
+ preferEndRefreshButton: false,
967
+ forceNavigate: true,
968
+ targetUrl: targetUrl || RECOMMEND_TARGET_URL,
969
+ forceRecentNotView: effectiveForceRecentNotView,
970
+ cardTimeoutMs,
876
971
  buttonSettleMs: refreshButtonSettleMs,
877
972
  reloadSettleMs: refreshReloadSettleMs
878
973
  });
879
974
  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
- };
975
+ if (refreshResult.ok) {
976
+ blockingPanelClose = await closeRecommendBlockingPanels(client, {
977
+ attemptsLimit: 2,
978
+ rootState: refreshResult.root_state || rootState
979
+ });
980
+ }
981
+ const compactRefresh = {
982
+ ...compactRefreshAttempt(refreshResult),
983
+ context_recovery: true,
984
+ recovery_reason: reason,
985
+ trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
986
+ account_rights_panel_close: compactCloseResult(blockingPanelClose),
987
+ elapsed_ms: Date.now() - started
988
+ };
894
989
  refreshAttempts.push(compactRefresh);
895
990
  runControl.checkpoint({
896
991
  context_recovery: {
@@ -900,56 +995,62 @@ export async function runRecommendWorkflow({
900
995
  refresh: compactRefresh,
901
996
  counters: countRecommendResultStatuses(results, { greetCount })
902
997
  },
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
998
+ candidate_list: compactInfiniteListState(listState)
999
+ });
1000
+ if (!refreshResult.ok) {
1001
+ updateRecommendProgress({
1002
+ refresh_method: refreshResult.method || null,
1003
+ refresh_forced_recent_not_view: effectiveForceRecentNotView,
1004
+ recovery_reason: reason
910
1005
  });
911
1006
  throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
912
1007
  }
1008
+ if (refreshResult.current_city_only) {
1009
+ currentCityOnlyResult = refreshResult.current_city_only;
1010
+ }
1011
+ if (refreshResult.filter) {
1012
+ filterResult = refreshResult.filter;
1013
+ }
913
1014
  if (!blockingPanelClose?.closed) {
914
1015
  const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
915
1016
  panelError.refresh_attempt = compactRefresh;
916
1017
  throw panelError;
917
1018
  }
918
- rootState = refreshResult.root_state || await getRecommendRoots(client);
1019
+ rootState = refreshResult.root_state || await getRecommendRoots(client);
919
1020
  rootState = await ensureRecommendViewport(rootState, "recover_after");
920
1021
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
921
1022
  timeoutMs: cardTimeoutMs,
922
1023
  intervalMs: 300
923
1024
  });
924
1025
  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;
1026
+ reason: `context_recovery:${reason}`,
1027
+ round: contextRecoveryAttempts,
1028
+ method: refreshResult.method,
1029
+ metadata: {
1030
+ card_count: cardNodeIds.length,
1031
+ forced_recent_not_view: effectiveForceRecentNotView,
1032
+ counters: countRecommendResultStatuses(results, { greetCount })
1033
+ }
1034
+ });
1035
+ listEndReason = "";
1036
+ updateRecommendProgress({
1037
+ card_count: cardNodeIds.length,
1038
+ refresh_method: refreshResult.method || null,
1039
+ refresh_forced_recent_not_view: effectiveForceRecentNotView,
1040
+ recovery_reason: reason
1041
+ });
1042
+ return refreshResult;
942
1043
  }
943
1044
 
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);
1045
+ runControl.setPhase("recommend:cleanup");
1046
+ await closeRecommendDetail(client, { attemptsLimit: 2 });
1047
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
1048
+ await closeRecommendBlockingPanelsForRun("cleanup");
1049
+
1050
+ await runControl.waitIfPaused();
1051
+ runControl.throwIfCanceled();
1052
+ runControl.setPhase("recommend:roots");
1053
+ rootState = await getRecommendRoots(client);
953
1054
  rootState = await ensureRecommendViewport(rootState, "roots");
954
1055
  runControl.checkpoint({
955
1056
  iframe_selector: rootState.iframe.selector,
@@ -987,34 +1088,55 @@ export async function runRecommendWorkflow({
987
1088
  throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
988
1089
  }
989
1090
  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");
1091
+ rootState = await ensureRecommendViewport(rootState, "page_scope");
1092
+ runControl.checkpoint({
1093
+ page_scope: compactPageScopeSelection(pageScopeSelection)
1094
+ });
1095
+
1096
+ const initialFilterStages = await applyRecommendFilterEnvelopeStages(normalizedFilter, {
1097
+ applyCurrentCityOnly: async () => {
1098
+ await runControl.waitIfPaused();
1099
+ runControl.throwIfCanceled();
1100
+ runControl.setPhase("recommend:current-city-only");
1101
+ const result = await ensureRecommendCurrentCityOnly(
1102
+ client,
1103
+ rootState.iframe.documentNodeId,
1104
+ { enabled: normalizedFilter.currentCityOnly }
1105
+ );
1106
+ rootState = await getRecommendRoots(client);
1107
+ rootState = await ensureRecommendViewport(rootState, "current_city_only");
1108
+ runControl.checkpoint({
1109
+ current_city_only: compactCurrentCityOnlyResult(result)
1110
+ });
1111
+ return result;
1112
+ },
1113
+ applyFilterPanel: async () => {
1114
+ await runControl.waitIfPaused();
1115
+ runControl.throwIfCanceled();
1116
+ runControl.setPhase("recommend:filter");
1117
+ const result = await selectAndConfirmFirstSafeFilter(
1118
+ client,
1119
+ rootState.iframe.documentNodeId,
1120
+ buildRecommendFilterSelectionOptions(normalizedFilter)
1121
+ );
1122
+ if (!result.confirmed) {
1123
+ throw new Error("Recommend run filter selection was not confirmed");
1124
+ }
1125
+ rootState = await getRecommendRoots(client);
1126
+ rootState = await ensureRecommendViewport(rootState, "filter");
1127
+ runControl.checkpoint({
1128
+ filter: compactFilterResult(result)
1129
+ });
1130
+ return result;
1131
+ }
1132
+ });
1133
+ currentCityOnlyResult = initialFilterStages.current_city_only;
1134
+ filterResult = initialFilterStages.filter;
1135
+
1136
+ await runControl.waitIfPaused();
1137
+ runControl.throwIfCanceled();
1138
+ runControl.setPhase("recommend:cards");
1139
+ rootState = await ensureRecommendViewport(rootState, "cards");
1018
1140
  cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1019
1141
  timeoutMs: cardTimeoutMs,
1020
1142
  intervalMs: 300
@@ -1039,11 +1161,11 @@ export async function runRecommendWorkflow({
1039
1161
  client,
1040
1162
  state: listState,
1041
1163
  maxScrolls: listMaxScrolls,
1042
- stableSignatureLimit: listStableSignatureLimit,
1043
- wheelDeltaY: listWheelDeltaY,
1044
- settleMs: listSettleMs,
1045
- listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1046
- fallbackPoint: listFallbackResolver,
1164
+ stableSignatureLimit: listStableSignatureLimit,
1165
+ wheelDeltaY: listWheelDeltaY,
1166
+ settleMs: listSettleMs,
1167
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1168
+ fallbackPoint: listFallbackResolver,
1047
1169
  findNodeIds: async () => {
1048
1170
  let currentRootState = await getRecommendRoots(client);
1049
1171
  currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
@@ -1084,55 +1206,61 @@ export async function runRecommendWorkflow({
1084
1206
  runControl.setPhase("recommend:refresh");
1085
1207
  refreshRounds += 1;
1086
1208
  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);
1209
+ rootState,
1210
+ jobLabel,
1211
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1212
+ fallbackPageScope: normalizedFallbackPageScope,
1213
+ filter: normalizedFilter,
1214
+ forceRecentNotView: normalizedFilter.enabled,
1215
+ cardTimeoutMs,
1216
+ buttonSettleMs: refreshButtonSettleMs,
1217
+ reloadSettleMs: refreshReloadSettleMs
1218
+ });
1219
+ const compactRefresh = compactRefreshAttempt(refreshResult);
1098
1220
  refreshAttempts.push(compactRefresh);
1099
1221
  runControl.checkpoint({
1100
1222
  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
- });
1223
+ refresh: compactRefresh
1224
+ });
1225
+ updateRecommendProgress({
1226
+ card_count: refreshResult.card_count || cardNodeIds.length,
1227
+ refresh_method: refreshResult.method || null,
1228
+ refresh_forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view),
1229
+ list_end_reason: listEndReason
1230
+ });
1109
1231
  if (refreshResult.ok) {
1232
+ if (refreshResult.current_city_only) {
1233
+ currentCityOnlyResult = refreshResult.current_city_only;
1234
+ }
1235
+ if (refreshResult.filter) {
1236
+ filterResult = refreshResult.filter;
1237
+ }
1110
1238
  rootState = refreshResult.root_state || await getRecommendRoots(client);
1111
1239
  rootState = await ensureRecommendViewport(rootState, "refresh_after");
1112
- cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1113
- timeoutMs: cardTimeoutMs,
1114
- intervalMs: 300
1240
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1241
+ timeoutMs: cardTimeoutMs,
1242
+ intervalMs: 300
1115
1243
  });
1116
1244
  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
- });
1245
+ reason: listEndReason,
1246
+ round: refreshRounds,
1247
+ method: refreshResult.method,
1248
+ metadata: {
1249
+ card_count: cardNodeIds.length,
1250
+ forced_recent_not_view: Boolean(refreshResult.forced_recent_not_view)
1251
+ }
1252
+ });
1125
1253
  listEndReason = "";
1126
1254
  continue;
1127
1255
  }
1128
- throw createRecommendRefreshFailureError(compactRefresh, {
1129
- listEndReason,
1130
- targetCount: targetPassCount,
1131
- passedCount: countPassedResults(results)
1132
- });
1133
- }
1134
- break;
1135
- }
1256
+ throw createRecommendRefreshFailureError(compactRefresh, {
1257
+ listEndReason,
1258
+ targetCount: targetPassCount,
1259
+ passedCount: countPassedResults(results)
1260
+ });
1261
+ }
1262
+ break;
1263
+ }
1136
1264
 
1137
1265
  const index = results.length;
1138
1266
  let cardNodeId = nextCandidateResult.item.node_id;
@@ -1140,42 +1268,42 @@ export async function runRecommendWorkflow({
1140
1268
  let cardCandidate = nextCandidateResult.item.candidate;
1141
1269
 
1142
1270
  let screeningCandidate = cardCandidate;
1143
- let detailResult = null;
1144
- let recoverableDetailError = null;
1145
- let colleagueContact = null;
1146
- let skipRecentColleagueContact = false;
1147
- let detailStep = "not_started";
1271
+ let detailResult = null;
1272
+ let recoverableDetailError = null;
1273
+ let colleagueContact = null;
1274
+ let skipRecentColleagueContact = false;
1275
+ let detailStep = "not_started";
1148
1276
  if (index < effectiveDetailLimit) {
1149
1277
  try {
1150
1278
  await runControl.waitIfPaused();
1151
1279
  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, {
1280
+ runControl.setPhase("recommend:detail");
1281
+ detailStep = "ensure_viewport";
1282
+ rootState = await ensureRecommendViewport(rootState, "detail");
1283
+ const blockingPanelClose = await closeRecommendBlockingPanels(client, {
1284
+ attemptsLimit: 2,
1285
+ rootState
1286
+ });
1287
+ if (!blockingPanelClose?.closed) {
1288
+ const panelError = createRecommendBlockingPanelCloseFailureError(
1289
+ blockingPanelClose,
1290
+ "before_detail_open"
1291
+ );
1292
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1293
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
1294
+ await recoverAndReapplyRecommendContext("account_rights_panel_before_detail", panelError, {
1295
+ forceRecentNotView: true
1296
+ });
1297
+ continue;
1298
+ }
1299
+ if (blockingPanelClose.already_closed === false) {
1300
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1301
+ }
1302
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1303
+ detailStep = "open_detail";
1304
+ networkRecorder.clear();
1305
+ await maybeHumanActionCooldown("before_detail_open", timings);
1306
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1179
1307
  cardNodeId,
1180
1308
  candidateKey,
1181
1309
  cardCandidate,
@@ -1186,52 +1314,52 @@ export async function runRecommendWorkflow({
1186
1314
  });
1187
1315
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
1188
1316
  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(
1317
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
1318
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
1319
+ screeningCandidate = cardCandidate;
1320
+ if (shouldSkipRecentColleagueContacted) {
1321
+ detailStep = "check_colleague_contact";
1322
+ try {
1323
+ colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
1324
+ client,
1325
+ openedDetail.detail_state,
1326
+ {
1327
+ referenceDate: colleagueContactReferenceDate,
1328
+ windowDays: normalizedColleagueContactWindowDays
1329
+ }
1330
+ ));
1331
+ if (colleagueContact?.recent) {
1332
+ skipRecentColleagueContact = true;
1333
+ detailResult = {
1334
+ candidate: screeningCandidate,
1335
+ detail: {
1336
+ popup_text: "",
1337
+ resume_text: ""
1338
+ },
1339
+ colleague_contact: colleagueContact,
1340
+ cv_acquisition: {
1341
+ source: "skipped_recent_colleague_contact",
1342
+ skipped: true,
1343
+ reason: "skipped_recent_colleague_contact"
1344
+ }
1345
+ };
1346
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1347
+ await maybeHumanActionCooldown("after_detail_close", timings);
1348
+ }
1349
+ } catch (error) {
1350
+ colleagueContact = {
1351
+ checked: false,
1352
+ recent: false,
1353
+ reason: "inspection_failed",
1354
+ error: error?.message || String(error),
1355
+ window_days: normalizedColleagueContactWindowDays
1356
+ };
1357
+ }
1358
+ }
1359
+ if (!skipRecentColleagueContact) {
1360
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1361
+ detailStep = "wait_network";
1362
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1235
1363
  waitForRecommendDetailNetworkEvents,
1236
1364
  networkRecorder,
1237
1365
  {
@@ -1255,10 +1383,10 @@ export async function runRecommendWorkflow({
1255
1383
  networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1256
1384
  networkParseIntervalMs: 250
1257
1385
  });
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);
1386
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1387
+ if (colleagueContact) detailResult.colleague_contact = colleagueContact;
1388
+
1389
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1262
1390
  let source = "network";
1263
1391
  let imageEvidence = null;
1264
1392
  let captureTarget = null;
@@ -1295,12 +1423,12 @@ export async function runRecommendWorkflow({
1295
1423
  resizeMaxWidth: 1100,
1296
1424
  captureViewport: false,
1297
1425
  padding: 0,
1298
- maxScreenshots: maxImagePages,
1299
- wheelDeltaY: imageWheelDeltaY,
1300
- settleMs: 350,
1301
- scrollMethod: "dom-anchor-fallback-input",
1302
- scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1303
- stepTimeoutMs: 45000,
1426
+ maxScreenshots: maxImagePages,
1427
+ wheelDeltaY: imageWheelDeltaY,
1428
+ settleMs: 350,
1429
+ scrollMethod: "dom-anchor-fallback-input",
1430
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1431
+ stepTimeoutMs: 45000,
1304
1432
  totalTimeoutMs: 90000,
1305
1433
  duplicateStopCount: 1,
1306
1434
  skipDuplicateScreenshots: true,
@@ -1324,14 +1452,14 @@ export async function runRecommendWorkflow({
1324
1452
  const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1325
1453
  if (recoveryCount < 1) {
1326
1454
  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
- });
1455
+ timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
1456
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1457
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1458
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1459
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1460
+ await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
1461
+ forceRecentNotView: true
1462
+ });
1335
1463
  continue;
1336
1464
  }
1337
1465
  imageEvidence = createRecoverableImageCaptureEvidence(error, {
@@ -1370,73 +1498,73 @@ export async function runRecommendWorkflow({
1370
1498
  image_evidence: summarizeImageEvidence(imageEvidence),
1371
1499
  capture_target: captureTarget || null,
1372
1500
  capture_target_wait: captureTargetWait
1373
- };
1374
- screeningCandidate = detailResult.candidate;
1375
- }
1376
- } catch (error) {
1501
+ };
1502
+ screeningCandidate = detailResult.candidate;
1503
+ }
1504
+ } catch (error) {
1377
1505
  if (!isRecoverableRecommendDetailError(error)) throw error;
1378
1506
  const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1379
1507
  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
- });
1508
+ candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1509
+ timings.detail_recovery_trigger = compactRecoverableDetailError(error);
1510
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1511
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1512
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1513
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1514
+ await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
1515
+ forceRecentNotView: true
1516
+ });
1389
1517
  continue;
1390
1518
  }
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
- }
1519
+ recoverableDetailError = error;
1520
+ detailResult = null;
1521
+ timings.detail_recovered_error = compactRecoverableDetailError(error);
1522
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1523
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1524
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1525
+ }
1398
1526
  }
1399
1527
 
1400
1528
  await runControl.waitIfPaused();
1401
1529
  runControl.throwIfCanceled();
1402
1530
  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) {
1531
+ let llmResult = null;
1532
+ if (useLlmScreening) {
1533
+ if (skipRecentColleagueContact || recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1534
+ llmResult = null;
1535
+ } else if (!llmConfig) {
1408
1536
  llmResult = createMissingLlmConfigResult();
1409
1537
  } else {
1410
1538
  try {
1411
1539
  const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
1412
1540
  ? "vision_model_ms"
1413
1541
  : "text_model_ms";
1414
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1415
- candidate: screeningCandidate,
1416
- criteria,
1417
- config: llmConfig,
1418
- timeoutMs: llmTimeoutMs,
1542
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1543
+ candidate: screeningCandidate,
1544
+ criteria,
1545
+ config: llmConfig,
1546
+ timeoutMs: llmTimeoutMs,
1419
1547
  imageEvidence: detailResult?.image_evidence || null,
1420
1548
  maxImages: llmImageLimit,
1421
1549
  imageDetail: llmImageDetail
1422
1550
  }));
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
1551
+ } catch (error) {
1552
+ if (isFatalLlmProviderError(error)) {
1553
+ throw createFatalLlmRunError(error, {
1554
+ domain: "recommend",
1555
+ candidate: screeningCandidate
1556
+ });
1557
+ }
1558
+ llmResult = createFailedLlmScreeningResult(error);
1559
+ }
1560
+ }
1561
+ if (detailResult) detailResult.llm_result = llmResult;
1562
+ }
1563
+ const screening = skipRecentColleagueContact
1564
+ ? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
1565
+ : recoverableDetailError
1566
+ ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1567
+ : detailResult?.image_evidence?.ok === false
1440
1568
  ? createImageCaptureFailureScreening(screeningCandidate, {
1441
1569
  code: detailResult.image_evidence.error_code,
1442
1570
  message: detailResult.image_evidence.error
@@ -1444,17 +1572,17 @@ export async function runRecommendWorkflow({
1444
1572
  : useLlmScreening
1445
1573
  ? llmResultToScreening(llmResult, screeningCandidate)
1446
1574
  : 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, {
1575
+ let actionDiscovery = null;
1576
+ let postActionResult = null;
1577
+ let closeFailureError = null;
1578
+ let closeRecoveryFailure = null;
1579
+ if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
1580
+ const postActionStarted = Date.now();
1581
+ await runControl.waitIfPaused();
1582
+ runControl.throwIfCanceled();
1583
+ runControl.setPhase("recommend:post-action");
1584
+ await maybeHumanActionCooldown("before_post_action", timings);
1585
+ actionDiscovery = await waitForRecommendDetailActionControls(client, {
1458
1586
  timeoutMs: actionTimeoutMs,
1459
1587
  intervalMs: actionIntervalMs,
1460
1588
  requireAny: true
@@ -1473,34 +1601,34 @@ export async function runRecommendWorkflow({
1473
1601
  greetCount += 1;
1474
1602
  }
1475
1603
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1476
- }
1477
- if (detailResult && closeDetail && !detailResult.close_result?.closed) {
1478
- 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 = {
1604
+ }
1605
+ if (detailResult && closeDetail && !detailResult.close_result?.closed) {
1606
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1607
+ await maybeHumanActionCooldown("after_detail_close", timings);
1608
+ if (!detailResult.close_result?.closed) {
1609
+ closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
1610
+ try {
1611
+ const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeFailureError, {
1612
+ forceRecentNotView: true
1613
+ });
1614
+ detailResult.cv_acquisition = {
1615
+ ...(detailResult.cv_acquisition || {}),
1616
+ close_recovery: {
1617
+ ok: Boolean(recovery.ok),
1618
+ method: recovery.method || "",
1619
+ forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
1620
+ card_count: recovery.card_count || 0
1621
+ }
1622
+ };
1623
+ } catch (error) {
1624
+ closeRecoveryFailure = error;
1625
+ detailResult.cv_acquisition = {
1498
1626
  ...(detailResult.cv_acquisition || {}),
1499
1627
  close_recovery: {
1500
1628
  ok: false,
1501
1629
  reason: "context_recovery_failed",
1502
1630
  error: error?.message || String(error),
1503
- forced_recent_not_view: true
1631
+ forced_recent_not_view: Boolean(normalizedFilter.enabled)
1504
1632
  }
1505
1633
  };
1506
1634
  }
@@ -1516,14 +1644,14 @@ export async function runRecommendWorkflow({
1516
1644
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1517
1645
  screening: compactScreening(screening),
1518
1646
  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,
1647
+ post_action: postActionResult,
1648
+ error: recoverableDetailError
1649
+ ? compactRecoverableDetailError(recoverableDetailError)
1650
+ : closeRecoveryFailure
1651
+ ? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
1652
+ : detailResult?.image_evidence?.ok === false
1653
+ ? compactError({
1654
+ code: detailResult.image_evidence.error_code,
1527
1655
  message: detailResult.image_evidence.error
1528
1656
  }, "IMAGE_CAPTURE_FAILED")
1529
1657
  : null,
@@ -1558,66 +1686,67 @@ export async function runRecommendWorkflow({
1558
1686
  error: compactResult.error,
1559
1687
  post_action: postActionResult
1560
1688
  }
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;
1689
+ });
1690
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1691
+
1692
+ if (closeRecoveryFailure) {
1693
+ throw closeRecoveryFailure;
1694
+ }
1695
+
1696
+ if (postActionResult?.stop_run) {
1697
+ listEndReason = postActionResult.reason || "post_action_stop";
1698
+ break;
1699
+ }
1700
+
1701
+ if (effectiveHumanRestEnabled) {
1702
+ const restStarted = Date.now();
1703
+ const restResult = await humanRestController.takeBreakIfNeeded({
1704
+ sleepFn: (ms) => runControl.sleep(ms)
1705
+ });
1706
+ const restElapsed = Date.now() - restStarted;
1707
+ if (restResult.rested) {
1708
+ recordHumanEvent({
1709
+ kind: "rest",
1710
+ pause_ms: restResult.pause_ms || restElapsed,
1711
+ events: restResult.events || []
1712
+ });
1713
+ compactResult.human_rest = restResult;
1714
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1715
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1716
+ updateRecommendProgress({
1717
+ human_rest_level: effectiveHumanBehavior.restLevel,
1718
+ human_rest_last: restResult
1719
+ });
1720
+ }
1721
+ }
1722
+
1723
+ if (delayMs > 0) {
1724
+ const sleepStarted = Date.now();
1725
+ await runControl.sleep(delayMs);
1726
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1727
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1600
1728
  }
1601
1729
  }
1602
1730
 
1603
1731
  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),
1732
+ return {
1733
+ domain: "recommend",
1734
+ target_url: targetUrl,
1735
+ job_selection: compactJobSelection(jobSelection),
1736
+ page_scope: compactPageScopeSelection(pageScopeSelection),
1737
+ current_city_only: compactCurrentCityOnlyResult(currentCityOnlyResult),
1738
+ filter: compactFilterResult(filterResult),
1739
+ card_count: cardNodeIds.length,
1740
+ candidate_list: compactInfiniteListState(listState),
1612
1741
  viewport_health: {
1613
1742
  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,
1620
- refresh_rounds: refreshRounds,
1743
+ events: viewportGuard.getEvents()
1744
+ },
1745
+ human_behavior: effectiveHumanBehavior,
1746
+ human_rest: humanRestController.getState(),
1747
+ last_human_event: lastHumanEvent,
1748
+ list_end_reason: listEndReason || null,
1749
+ refresh_rounds: refreshRounds,
1621
1750
  refresh_attempts: refreshAttempts,
1622
1751
  context_recoveries: contextRecoveryAttempts,
1623
1752
  ...countRecommendResultStatuses(results, { greetCount }),
@@ -1667,30 +1796,30 @@ export function createRecommendRunService({
1667
1796
  actionIntervalMs = 500,
1668
1797
  actionAfterClickDelayMs = 900,
1669
1798
  screeningMode = "llm",
1670
- llmConfig = null,
1671
- llmTimeoutMs = 120000,
1672
- llmImageLimit = 8,
1673
- llmImageDetail = "high",
1674
- imageOutputDir = "",
1675
- humanRestEnabled = false,
1676
- humanBehavior = null,
1677
- skipRecentColleagueContacted = true,
1678
- colleagueContactWindowDays = 14,
1679
- name = "recommend-domain-run"
1680
- } = {}) {
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
- });
1692
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1693
- const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1799
+ llmConfig = null,
1800
+ llmTimeoutMs = 120000,
1801
+ llmImageLimit = 8,
1802
+ llmImageDetail = "high",
1803
+ imageOutputDir = "",
1804
+ humanRestEnabled = false,
1805
+ humanBehavior = null,
1806
+ skipRecentColleagueContacted = true,
1807
+ colleagueContactWindowDays = 14,
1808
+ name = "recommend-domain-run"
1809
+ } = {}) {
1810
+ if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1811
+ const normalizedFilter = normalizeFilter(filter);
1812
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1813
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1814
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1815
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1816
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
1817
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
1818
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1819
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1820
+ });
1821
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1822
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1694
1823
  const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1695
1824
  return manager.startRun({
1696
1825
  runId,
@@ -1699,16 +1828,17 @@ export function createRecommendRunService({
1699
1828
  context: {
1700
1829
  domain: "recommend",
1701
1830
  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,
1831
+ criteria_present: Boolean(criteria),
1832
+ job_label: jobLabel || "",
1833
+ requested_page_scope: requestedPageScope,
1834
+ fallback_page_scope: normalizedFallbackPageScope,
1835
+ filter: normalizedFilter,
1836
+ current_city_only_requested: normalizedFilter.currentCityOnly,
1837
+ max_candidates: maxCandidates,
1838
+ max_candidates_semantics: "passed_candidates",
1839
+ detail_limit: normalizedDetailLimit,
1840
+ close_detail: closeDetail,
1841
+ cv_acquisition_mode: cvAcquisitionMode,
1712
1842
  max_image_pages: maxImagePages,
1713
1843
  image_wheel_delta_y: imageWheelDeltaY,
1714
1844
  list_max_scrolls: listMaxScrolls,
@@ -1726,45 +1856,48 @@ export function createRecommendRunService({
1726
1856
  action_timeout_ms: actionTimeoutMs,
1727
1857
  screening_mode: normalizedScreeningMode,
1728
1858
  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,
1737
- human_behavior: effectiveHumanBehavior,
1738
- human_rest_level: effectiveHumanBehavior.restLevel,
1739
- human_rest_enabled: effectiveHumanRestEnabled
1740
- },
1741
- progress: {
1859
+ llm_timeout_ms: llmTimeoutMs,
1860
+ llm_image_limit: llmImageLimit,
1861
+ llm_image_detail: llmImageDetail,
1862
+ image_output_dir: imageOutputDir || "",
1863
+ skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
1864
+ colleague_contact_window_days: normalizedColleagueContactWindowDays,
1865
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1866
+ human_behavior_profile: effectiveHumanBehavior.profile,
1867
+ human_behavior: effectiveHumanBehavior,
1868
+ human_rest_level: effectiveHumanBehavior.restLevel,
1869
+ human_rest_enabled: effectiveHumanRestEnabled
1870
+ },
1871
+ progress: {
1742
1872
  card_count: 0,
1743
1873
  target_count: candidateLimit,
1744
1874
  target_count_semantics: "passed_candidates",
1745
1875
  processed: 0,
1746
1876
  screened: 0,
1747
1877
  detail_opened: 0,
1748
- llm_screened: 0,
1749
- passed: 0,
1750
- skipped: 0,
1751
- greet_count: 0,
1878
+ llm_screened: 0,
1879
+ passed: 0,
1880
+ skipped: 0,
1881
+ greet_count: 0,
1752
1882
  post_action_clicked: 0,
1753
- image_capture_failed: 0,
1754
- detail_open_failed: 0,
1883
+ image_capture_failed: 0,
1884
+ detail_open_failed: 0,
1755
1885
  transient_recovered: 0,
1756
1886
  colleague_contact_checked: 0,
1757
1887
  recent_colleague_contact_skipped: 0,
1758
1888
  colleague_contact_panel_missing: 0,
1759
1889
  context_recoveries: 0,
1890
+ current_city_only_requested: normalizedFilter.currentCityOnly,
1891
+ current_city_only_effective: null,
1892
+ current_city_only_unavailable: false,
1760
1893
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1761
1894
  human_behavior_profile: effectiveHumanBehavior.profile,
1762
1895
  human_rest_level: effectiveHumanBehavior.restLevel,
1763
1896
  human_rest_enabled: effectiveHumanRestEnabled,
1764
1897
  human_rest_count: 0,
1765
- human_rest_ms: 0,
1766
- last_human_event: null
1767
- },
1898
+ human_rest_ms: 0,
1899
+ last_human_event: null
1900
+ },
1768
1901
  checkpoint: {},
1769
1902
  task: (runControl) => workflow({
1770
1903
  client,
@@ -1799,17 +1932,17 @@ export function createRecommendRunService({
1799
1932
  actionAfterClickDelayMs,
1800
1933
  screeningMode: normalizedScreeningMode,
1801
1934
  llmConfig,
1802
- llmTimeoutMs,
1803
- llmImageLimit,
1804
- llmImageDetail,
1805
- imageOutputDir,
1806
- humanRestEnabled: effectiveHumanRestEnabled,
1807
- humanBehavior: effectiveHumanBehavior,
1808
- skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
1809
- colleagueContactWindowDays: normalizedColleagueContactWindowDays
1810
- }, runControl)
1811
- });
1812
- }
1935
+ llmTimeoutMs,
1936
+ llmImageLimit,
1937
+ llmImageDetail,
1938
+ imageOutputDir,
1939
+ humanRestEnabled: effectiveHumanRestEnabled,
1940
+ humanBehavior: effectiveHumanBehavior,
1941
+ skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
1942
+ colleagueContactWindowDays: normalizedColleagueContactWindowDays
1943
+ }, runControl)
1944
+ });
1945
+ }
1813
1946
 
1814
1947
  return {
1815
1948
  startRecommendRun,