@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +14 -8
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/core/boss-cards/index.js +199 -199
  14. package/src/core/browser/index.js +291 -114
  15. package/src/core/capture/index.js +2930 -1201
  16. package/src/core/cv-acquisition/index.js +512 -238
  17. package/src/core/cv-capture-target/index.js +513 -299
  18. package/src/core/greet-quota/index.js +71 -71
  19. package/src/core/infinite-list/index.js +11 -2
  20. package/src/core/reporting/legacy-csv.js +12 -12
  21. package/src/core/run/detached-launcher.js +305 -0
  22. package/src/core/run/index.js +112 -42
  23. package/src/core/run/timing.js +33 -33
  24. package/src/core/run/windows-detached-worker.ps1 +106 -0
  25. package/src/core/screening/index.js +2135 -2135
  26. package/src/core/self-heal/index.js +989 -973
  27. package/src/core/self-heal/viewport.js +1505 -564
  28. package/src/detached-worker.js +99 -99
  29. package/src/domains/chat/action-journal.js +443 -0
  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 +1684 -401
  33. package/src/domains/chat/index.js +8 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +157 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +1801 -760
  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 +515 -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 +92 -92
  44. package/src/domains/recommend/detail.js +12 -3
  45. package/src/domains/recommend/filters.js +611 -611
  46. package/src/domains/recommend/index.js +9 -9
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -736
  49. package/src/domains/recommend/refresh.js +410 -329
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1783 -592
  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 +1817 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +124 -5
  64. package/src/parser.js +1296 -1296
  65. package/src/recommend-mcp.js +515 -80
  66. package/src/recommend-scheduler.js +66 -0
@@ -1,7 +1,7 @@
1
- import {
2
- captureScrolledNodeScreenshots,
3
- captureViewportScreenshot
4
- } from "../../core/capture/index.js";
1
+ import {
2
+ captureScrolledNodeScreenshots,
3
+ captureViewportScreenshot
4
+ } from "../../core/capture/index.js";
5
5
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
6
6
  import {
7
7
  clickPoint,
@@ -13,16 +13,24 @@ import {
13
13
  scrollNodeIntoView,
14
14
  sleep
15
15
  } from "../../core/browser/index.js";
16
- import {
17
- compactCvAcquisitionState,
16
+ import {
17
+ attemptImageCaptureCheckpointResume,
18
+ compactCvAcquisitionState,
19
+ createImageCaptureWorkflowRetryTracker,
20
+ createRequiredImageEvidenceFailure,
21
+ imageCaptureResumeCheckpoint,
18
22
  countParsedNetworkProfiles,
19
23
  createCvAcquisitionState,
20
24
  DEFAULT_MAX_IMAGE_PAGES,
21
- getCvNetworkWaitPlan,
22
- recordCvImageFallback,
25
+ getCvNetworkWaitPlan,
26
+ isIncompleteImageEvidence,
27
+ isRecoverableImageCaptureWorkflowError,
28
+ recordCvImageFallback,
23
29
  recordCvNetworkHit,
24
- recordCvNetworkMiss,
25
- summarizeImageEvidence,
30
+ recordCvNetworkMiss,
31
+ reacquireImageCaptureResumeTarget,
32
+ requireCompleteImageEvidence,
33
+ summarizeImageEvidence,
26
34
  waitForCvNetworkEvents
27
35
  } from "../../core/cv-acquisition/index.js";
28
36
  import {
@@ -35,66 +43,323 @@ import {
35
43
  resolveInfiniteListFallbackPoint
36
44
  } from "../../core/infinite-list/index.js";
37
45
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
38
- import {
39
- createRunLifecycleManager,
40
- RunCanceledError
41
- } from "../../core/run/index.js";
46
+ import {
47
+ createRunLifecycleManager,
48
+ RunCanceledError
49
+ } from "../../core/run/index.js";
42
50
  import {
43
51
  addTiming,
44
52
  imageEvidenceFilePath,
45
53
  measureTiming
46
54
  } from "../../core/run/timing.js";
47
- import {
48
- callScreeningLlm,
49
- createFatalLlmRunError,
50
- isFatalLlmProviderError,
51
- normalizeText,
52
- screenCandidate
53
- } from "../../core/screening/index.js";
55
+ import {
56
+ callScreeningLlm,
57
+ createFatalLlmRunError,
58
+ isFatalLlmProviderError,
59
+ normalizeText,
60
+ screenCandidate
61
+ } from "../../core/screening/index.js";
54
62
  import {
55
63
  CHAT_BOTTOM_MARKER_SELECTORS,
56
64
  CHAT_CARD_SELECTORS,
57
65
  CHAT_LIST_CONTAINER_SELECTORS,
58
66
  CHAT_TARGET_URL
59
67
  } from "./constants.js";
60
- import {
61
- chatCandidateKeyFromProfile,
68
+ import {
69
+ chatCandidateKeyFromProfile,
62
70
  findChatCandidateNodeIdById,
63
71
  readChatCardCandidate,
64
- waitForChatCandidateNodeIds
65
- } from "./cards.js";
66
- import {
72
+ waitForChatCandidateNodeIds
73
+ } from "./cards.js";
74
+ import { createChatActionJournal } from "./action-journal.js";
75
+ import {
67
76
  closeChatBlockingPanels,
68
77
  closeChatResumeModal,
69
78
  createChatProfileNetworkRecorder,
70
- extractChatProfileCandidate,
71
- isChatOnlineResumeModalOpenFailureError,
72
- isUnsafeChatOnlineResumeLinkError,
73
- openChatOnlineResume,
74
- quickChatResumeModalOpenProbe,
75
- readChatActiveCandidateState,
76
- readChatConversationReadyState,
77
- requestChatResumeForPassedCandidate,
79
+ extractChatProfileCandidate,
80
+ isChatOnlineResumeModalOpenFailureError,
81
+ isUnsafeChatOnlineResumeLinkError,
82
+ openChatOnlineResume,
83
+ quickChatResumeModalOpenProbe,
84
+ readChatActiveCandidateState,
85
+ readChatConversationReadyState,
86
+ readChatRequestVerificationEvidence,
87
+ getChatGreetingMessageState,
88
+ requestChatResumeForPassedCandidate,
78
89
  selectChatMessageFilter,
79
90
  selectChatPrimaryLabel,
80
91
  waitForChatOnlineResumeButton,
81
- waitForChatProfileNetworkEvents,
82
- waitForChatResumeContent
92
+ waitForChatProfileNetworkEvents,
93
+ waitForChatResumeModal,
94
+ waitForChatResumeContent
83
95
  } from "./detail.js";
84
96
  import { selectChatJob } from "./jobs.js";
85
- import {
86
- getChatTopLevelState,
87
- isForbiddenChatResumeNavigationError,
88
- makeForbiddenChatResumeNavigationError,
89
- recoverChatShell
97
+ import {
98
+ getChatTopLevelState,
99
+ makeBossSecurityVerificationRequiredError,
100
+ isForbiddenChatResumeNavigationError,
101
+ makeForbiddenChatResumeNavigationError,
102
+ recoverChatShell
90
103
  } from "./page-guard.js";
91
- import { getChatRoots } from "./roots.js";
92
-
93
- const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
94
- const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS = 5000;
95
- const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS = 8000;
96
-
97
- function normalizeDetailSource(value) {
104
+ import { getChatRoots } from "./roots.js";
105
+
106
+ const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
107
+ export const CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS = 10000;
108
+ export const CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS = 15000;
109
+ export const CHAT_EDITOR_PRE_ACTION_RETRY_LIMIT = 1;
110
+ export const CHAT_EDITOR_PRE_ACTION_RETRY_SETTLE_MS = 1500;
111
+
112
+ export function shouldRetryChatEditorPreAction(error, journalRecord, retryCount = 0, {
113
+ retryLimit = CHAT_EDITOR_PRE_ACTION_RETRY_LIMIT
114
+ } = {}) {
115
+ const code = String(error?.code || error?.message || "").trim();
116
+ const state = String(journalRecord?.state || "").trim();
117
+ const count = Math.max(0, Math.floor(Number(retryCount) || 0));
118
+ const limit = Math.max(0, Math.floor(Number(retryLimit) || 0));
119
+ return code === "CHAT_EDITOR_MESSAGE_MISMATCH"
120
+ && state === "pre_action"
121
+ && count < limit;
122
+ }
123
+
124
+ export function sampleChatCollectCvProcessingFloorMs({
125
+ minMs = CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS,
126
+ maxMs = CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS,
127
+ random = Math.random
128
+ } = {}) {
129
+ const lower = Math.max(0, Math.round(Number(minMs) || 0));
130
+ const upper = Math.max(lower, Math.round(Number(maxMs) || lower));
131
+ const raw = typeof random === "function" ? Number(random()) : Number.NaN;
132
+ const unit = Number.isFinite(raw) ? Math.max(0, Math.min(1, raw)) : 0.5;
133
+ return Math.round(lower + (upper - lower) * unit);
134
+ }
135
+
136
+ export async function enforceChatCollectCvProcessingFloor({
137
+ enabled = true,
138
+ candidateStartedAt,
139
+ targetMs = null,
140
+ minMs = CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS,
141
+ maxMs = CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS,
142
+ random = Math.random,
143
+ nowFn = Date.now,
144
+ sleepFn = sleep
145
+ } = {}) {
146
+ if (!enabled) {
147
+ return {
148
+ enabled: false,
149
+ target_ms: 0,
150
+ elapsed_before_ms: 0,
151
+ delay_requested_ms: 0,
152
+ delay_elapsed_ms: 0,
153
+ elapsed_after_ms: 0,
154
+ verified: true
155
+ };
156
+ }
157
+ const readNow = typeof nowFn === "function" ? nowFn : Date.now;
158
+ const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
159
+ const startedAt = Number(candidateStartedAt);
160
+ if (!Number.isFinite(startedAt)) {
161
+ throw new Error("enforceChatCollectCvProcessingFloor requires candidateStartedAt");
162
+ }
163
+ const sampledTarget = targetMs == null
164
+ ? sampleChatCollectCvProcessingFloorMs({ minMs, maxMs, random })
165
+ : sampleChatCollectCvProcessingFloorMs({ minMs: targetMs, maxMs: targetMs, random });
166
+ const elapsedBefore = Math.max(0, Number(readNow()) - startedAt);
167
+ const delayStartedAt = Number(readNow());
168
+ let delayRequestedMs = 0;
169
+ let elapsedAfter = elapsedBefore;
170
+ let stagnantReads = 0;
171
+ while (elapsedAfter < sampledTarget) {
172
+ const remainingMs = Math.max(1, Math.ceil(sampledTarget - elapsedAfter));
173
+ delayRequestedMs += remainingMs;
174
+ await sleeper(remainingMs);
175
+ const nextElapsed = Math.max(0, Number(readNow()) - startedAt);
176
+ stagnantReads = nextElapsed > elapsedAfter ? 0 : stagnantReads + 1;
177
+ elapsedAfter = nextElapsed;
178
+ if (stagnantReads >= 3) {
179
+ const error = new Error("Chat collect-CV processing-floor clock did not advance after sleep");
180
+ error.code = "CHAT_COLLECT_CV_PROCESSING_FLOOR_CLOCK_STALLED";
181
+ error.target_ms = sampledTarget;
182
+ error.elapsed_ms = elapsedAfter;
183
+ throw error;
184
+ }
185
+ }
186
+ const delayElapsedMs = Math.max(0, Number(readNow()) - delayStartedAt);
187
+ return {
188
+ enabled: true,
189
+ target_ms: sampledTarget,
190
+ elapsed_before_ms: Math.round(elapsedBefore),
191
+ delay_requested_ms: Math.round(delayRequestedMs),
192
+ delay_elapsed_ms: Math.round(delayElapsedMs),
193
+ elapsed_after_ms: Math.round(elapsedAfter),
194
+ verified: elapsedAfter >= sampledTarget
195
+ };
196
+ }
197
+
198
+ function compactChatActionJournalRecord(record = null, filePath = null) {
199
+ if (!record) return null;
200
+ return {
201
+ schema_version: record.schema_version || 1,
202
+ action_key: record.action_key || null,
203
+ candidate_id: record.candidate_id || null,
204
+ state: record.state || null,
205
+ evidence: record.evidence || {},
206
+ created_at: record.created_at || null,
207
+ updated_at: record.updated_at || null,
208
+ first_run_id: record.first_run_id || null,
209
+ last_run_id: record.last_run_id || null,
210
+ file_path: filePath || null
211
+ };
212
+ }
213
+
214
+ function chatActionUnknownOrigin(record = null) {
215
+ const history = Array.isArray(record?.history) ? record.history : [];
216
+ const last = history[history.length - 1];
217
+ return record?.state === "outcome_unknown" ? last?.from_state || null : null;
218
+ }
219
+
220
+ export function reconcileChatRequestJournalFromExactExistingState({
221
+ actionJournal = null,
222
+ actionJournalScope = "",
223
+ actionJournalRecord = null,
224
+ candidateId = "",
225
+ selectionReadyState = null,
226
+ readyState = null,
227
+ runId = "",
228
+ greeting = "",
229
+ checkpointCritical = null
230
+ } = {}) {
231
+ const normalizedCandidateId = normalizeText(candidateId);
232
+ const expectedCandidateId = normalizeText(selectionReadyState?.expected_candidate_id || "");
233
+ const activeCandidateId = normalizeText(selectionReadyState?.active_candidate_id || "");
234
+ const exactCandidateBound = Boolean(
235
+ normalizedCandidateId
236
+ && selectionReadyState?.candidate_selection_verified === true
237
+ && expectedCandidateId === normalizedCandidateId
238
+ && activeCandidateId === normalizedCandidateId
239
+ );
240
+ const exactExistingRequestState = Boolean(
241
+ readyState?.already_requested_resume
242
+ || readyState?.attachment_resume_enabled
243
+ );
244
+ const recoverableRequestJournal = Boolean(
245
+ actionJournalRecord?.state === "request_in_flight"
246
+ || (
247
+ actionJournalRecord?.state === "outcome_unknown"
248
+ && chatActionUnknownOrigin(actionJournalRecord) === "request_in_flight"
249
+ )
250
+ );
251
+ if (
252
+ !exactCandidateBound
253
+ || !exactExistingRequestState
254
+ || !recoverableRequestJournal
255
+ || actionJournalRecord?.candidate_id !== normalizedCandidateId
256
+ || typeof actionJournal?.transition !== "function"
257
+ || typeof checkpointCritical !== "function"
258
+ ) {
259
+ return null;
260
+ }
261
+
262
+ const confirmationSource = readyState.already_requested_resume
263
+ ? "exact_requested_resume_state"
264
+ : "attachment_resume_available";
265
+ const transitioned = actionJournal.transition({
266
+ scope: actionJournalScope,
267
+ candidateId: normalizedCandidateId,
268
+ state: "request_confirmed",
269
+ runId,
270
+ greeting,
271
+ evidence: {
272
+ active_candidate_id: activeCandidateId,
273
+ reason: "recovered_from_exact_existing_request_state_before_screening",
274
+ message_observed: Boolean(readyState.already_requested_resume),
275
+ request_ready_state_observed: true,
276
+ request_confirmation_source: confirmationSource
277
+ }
278
+ });
279
+ const actionTransaction = compactChatActionJournalRecord(
280
+ transitioned.record,
281
+ transitioned.file_path
282
+ );
283
+ checkpointCritical({ action_transaction: actionTransaction });
284
+ return {
285
+ requested: false,
286
+ skipped: true,
287
+ satisfied: true,
288
+ reason: "request_confirmed_from_exact_existing_state",
289
+ initial_state: readyState,
290
+ journal_reconciled_existing_state: true,
291
+ action_transaction: actionTransaction
292
+ };
293
+ }
294
+
295
+ export function preserveChatRequestOutcomeUnknownWithoutReplay({
296
+ actionJournalRecord = null,
297
+ candidateId = "",
298
+ activeCandidateId = "",
299
+ readyEvidence = null,
300
+ messageEvidence = null
301
+ } = {}) {
302
+ const normalizedCandidateId = normalizeText(candidateId);
303
+ const normalizedActiveCandidateId = normalizeText(activeCandidateId);
304
+ const exactCandidateBound = Boolean(
305
+ normalizedCandidateId
306
+ && normalizedActiveCandidateId === normalizedCandidateId
307
+ && normalizeText(actionJournalRecord?.candidate_id || "") === normalizedCandidateId
308
+ );
309
+ if (
310
+ !exactCandidateBound
311
+ || actionJournalRecord?.state !== "outcome_unknown"
312
+ || chatActionUnknownOrigin(actionJournalRecord) !== "request_in_flight"
313
+ ) {
314
+ return null;
315
+ }
316
+
317
+ const requestMessageCount = Number(messageEvidence?.count);
318
+ const resumeAttachmentCount = Number(messageEvidence?.resume_attachment_count);
319
+ return {
320
+ requested: false,
321
+ skipped: true,
322
+ satisfied: false,
323
+ reason: "outcome_unknown_preserved_no_replay",
324
+ initial_state: readyEvidence || null,
325
+ journal_preserved_outcome_unknown: true,
326
+ no_replay: true,
327
+ action_unknown_origin: "request_in_flight",
328
+ request_verification: {
329
+ ready_state_readable: Boolean(readyEvidence),
330
+ message_state_readable: messageEvidence?.ok === true,
331
+ request_message_count: Number.isFinite(requestMessageCount) ? requestMessageCount : null,
332
+ resume_attachment_count: Number.isFinite(resumeAttachmentCount) ? resumeAttachmentCount : null,
333
+ request_marker_observed: Boolean(readyEvidence?.already_requested_resume),
334
+ attachment_resume_observed: Boolean(readyEvidence?.attachment_resume_enabled)
335
+ },
336
+ action_transaction: compactChatActionJournalRecord(actionJournalRecord)
337
+ };
338
+ }
339
+
340
+ export function applyChatProtectedOutcomeSkip(screening = null, postAction = null, candidate = null) {
341
+ if (postAction?.journal_preserved_outcome_unknown !== true) return screening;
342
+ return {
343
+ status: "skip",
344
+ passed: false,
345
+ score: 0,
346
+ reasons: [postAction.reason || "outcome_unknown_preserved_no_replay"],
347
+ candidate: candidate || screening?.candidate || null
348
+ };
349
+ }
350
+
351
+ function makeChatPostActionOutcomeUnknownError(candidateId, record, evidence = {}) {
352
+ const error = new Error(`POST_ACTION_OUTCOME_UNKNOWN: candidate=${candidateId || "unknown"}; state=${record?.state || "unknown"}`);
353
+ error.code = "POST_ACTION_OUTCOME_UNKNOWN";
354
+ error.retryable = false;
355
+ error.candidate_id = candidateId || null;
356
+ error.action_state = record?.state || null;
357
+ error.action_unknown_origin = chatActionUnknownOrigin(record);
358
+ error.action_evidence = evidence;
359
+ return error;
360
+ }
361
+
362
+ function normalizeDetailSource(value) {
98
363
  const normalized = String(value || "").trim().toLowerCase();
99
364
  return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
100
365
  }
@@ -116,35 +381,35 @@ function compactScreening(screening) {
116
381
 
117
382
  function compactLlmResult(llmResult) {
118
383
  if (!llmResult) return null;
119
- return {
120
- ok: Boolean(llmResult.ok),
121
- provider: llmResult.provider || null,
122
- passed: llmResult.passed,
123
- review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
124
- cot: llmResult.cot || llmResult.decision_cot || "",
125
- reasoning_content: llmResult.reasoning_content || "",
126
- raw_model_output: llmResult.raw_model_output || "",
127
- evidence_count: llmResult.evidence?.length || 0,
128
- usage: llmResult.usage || null,
384
+ return {
385
+ ok: Boolean(llmResult.ok),
386
+ provider: llmResult.provider || null,
387
+ passed: llmResult.passed,
388
+ review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
389
+ cot: llmResult.cot || llmResult.decision_cot || "",
390
+ reasoning_content: llmResult.reasoning_content || "",
391
+ raw_model_output: llmResult.raw_model_output || "",
392
+ evidence_count: llmResult.evidence?.length || 0,
393
+ usage: llmResult.usage || null,
129
394
  finish_reason: llmResult.finish_reason || null,
130
- image_input_count: llmResult.image_input_count || 0,
131
- attempt_count: llmResult.attempt_count || 0,
132
- fallback_count: llmResult.fallback_count || 0,
133
- llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
134
- screening_strategy: llmResult.screening_strategy || "",
135
- fast_thinking_level: llmResult.fast_thinking_level || "",
136
- verify_thinking_level: llmResult.verify_thinking_level || "",
137
- verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
138
- verification_reason: llmResult.verification_reason || "",
139
- decision_source: llmResult.decision_source || "",
140
- fast_result: llmResult.fast_result || null,
141
- verify_result: llmResult.verify_result || null,
142
- error_code: llmResult.error_code || null,
143
- fatal: Boolean(llmResult.fatal),
144
- fatal_reason: llmResult.fatal_reason || "",
145
- error: llmResult.error || null
146
- };
147
- }
395
+ image_input_count: llmResult.image_input_count || 0,
396
+ attempt_count: llmResult.attempt_count || 0,
397
+ fallback_count: llmResult.fallback_count || 0,
398
+ llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
399
+ screening_strategy: llmResult.screening_strategy || "",
400
+ fast_thinking_level: llmResult.fast_thinking_level || "",
401
+ verify_thinking_level: llmResult.verify_thinking_level || "",
402
+ verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
403
+ verification_reason: llmResult.verification_reason || "",
404
+ decision_source: llmResult.decision_source || "",
405
+ fast_result: llmResult.fast_result || null,
406
+ verify_result: llmResult.verify_result || null,
407
+ error_code: llmResult.error_code || null,
408
+ fatal: Boolean(llmResult.fatal),
409
+ fatal_reason: llmResult.fatal_reason || "",
410
+ error: llmResult.error || null
411
+ };
412
+ }
148
413
 
149
414
  function compactCandidate(candidate) {
150
415
  return {
@@ -171,9 +436,9 @@ function compactChatJobGuard(result = null) {
171
436
  };
172
437
  }
173
438
 
174
- function compactDetail(detailResult) {
175
- if (!detailResult) return null;
176
- return {
439
+ function compactDetail(detailResult) {
440
+ if (!detailResult) return null;
441
+ return {
177
442
  popup_text_length: detailResult.detail?.popup_text?.length || 0,
178
443
  content_text_length: detailResult.detail?.content_text?.length || 0,
179
444
  resume_iframe_text_length: detailResult.detail?.resume_iframe_text?.length || 0,
@@ -213,10 +478,61 @@ export function makeChatResumeModalOpenBeforeCandidateClickError(closeResult = n
213
478
  return error;
214
479
  }
215
480
 
216
- export function isChatResumeModalCloseFailureError(error) {
217
- return error?.code === "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK"
218
- || /CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK/i.test(String(error?.message || error || ""));
219
- }
481
+ export function isChatResumeModalCloseFailureError(error) {
482
+ return error?.code === "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK"
483
+ || /CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK/i.test(String(error?.message || error || ""));
484
+ }
485
+
486
+ export function consumeChatDetailRecoveryBudget(recoveryCounts, candidateKey, {
487
+ retryLimit = 1
488
+ } = {}) {
489
+ if (!(recoveryCounts instanceof Map)) {
490
+ throw new TypeError("consumeChatDetailRecoveryBudget requires a recovery-count Map");
491
+ }
492
+ const key = String(candidateKey || "").trim() || "__unknown_candidate__";
493
+ const limit = Math.max(0, Math.floor(Number(retryLimit) || 0));
494
+ const previousCount = Math.max(0, Math.floor(Number(recoveryCounts.get(key)) || 0));
495
+ if (previousCount >= limit) {
496
+ return {
497
+ allowed: false,
498
+ previous_count: previousCount,
499
+ count: previousCount,
500
+ retry_limit: limit
501
+ };
502
+ }
503
+ const count = previousCount + 1;
504
+ recoveryCounts.set(key, count);
505
+ return {
506
+ allowed: true,
507
+ previous_count: previousCount,
508
+ count,
509
+ retry_limit: limit
510
+ };
511
+ }
512
+
513
+ export function compactChatCandidateSelectionReadyState(selectionReadyState = null) {
514
+ if (!selectionReadyState) return null;
515
+ const activeCandidate = selectionReadyState.activeCandidate || selectionReadyState.active_candidate || null;
516
+ return {
517
+ ok: selectionReadyState.ok === true,
518
+ reason: selectionReadyState.reason || null,
519
+ elapsed_ms: Number.isFinite(Number(selectionReadyState.elapsed_ms))
520
+ ? Number(selectionReadyState.elapsed_ms)
521
+ : null,
522
+ expected_candidate_id: normalizeText(selectionReadyState.expected_candidate_id || "") || null,
523
+ active_candidate_id: normalizeText(selectionReadyState.active_candidate_id || "") || null,
524
+ candidate_selection_verified: selectionReadyState.candidate_selection_verified === true,
525
+ active_candidate: activeCandidate
526
+ ? {
527
+ node_id: activeCandidate.node_id || null,
528
+ selector: activeCandidate.selector || null,
529
+ candidate_id: normalizeText(activeCandidate.candidate_id || "") || null,
530
+ label: normalizeText(activeCandidate.label || "") || null,
531
+ outer_html_length: activeCandidate.outer_html_length || 0
532
+ }
533
+ : null
534
+ };
535
+ }
220
536
 
221
537
  export function makeChatCandidateSelectionMismatchError(selected = null, candidate = null) {
222
538
  const expectedId = candidate?.id || selected?.ready?.expected_candidate_id || "";
@@ -229,10 +545,27 @@ export function makeChatCandidateSelectionMismatchError(selected = null, candida
229
545
  return error;
230
546
  }
231
547
 
232
- export function isChatCandidateSelectionMismatchError(error) {
233
- return error?.code === "CHAT_ACTIVE_CANDIDATE_MISMATCH"
234
- || /CHAT_ACTIVE_CANDIDATE_MISMATCH/i.test(String(error?.message || error || ""));
235
- }
548
+ export function isChatCandidateSelectionMismatchError(error) {
549
+ return error?.code === "CHAT_ACTIVE_CANDIDATE_MISMATCH"
550
+ || /CHAT_ACTIVE_CANDIDATE_MISMATCH/i.test(String(error?.message || error || ""));
551
+ }
552
+
553
+ export function requireExactChatCandidateSelection(selected = null, candidate = null) {
554
+ const candidateId = normalizeText(candidate?.id || "");
555
+ const selectionReadyState = selected?.ready || null;
556
+ const expectedCandidateId = normalizeText(selectionReadyState?.expected_candidate_id || "");
557
+ const activeCandidateId = normalizeText(selectionReadyState?.active_candidate_id || "");
558
+ const exactCandidateBound = Boolean(
559
+ candidateId
560
+ && selectionReadyState?.candidate_selection_verified === true
561
+ && expectedCandidateId === candidateId
562
+ && activeCandidateId === candidateId
563
+ );
564
+ if (!exactCandidateBound) {
565
+ throw makeChatCandidateSelectionMismatchError(selected, candidate);
566
+ }
567
+ return selectionReadyState;
568
+ }
236
569
 
237
570
  export async function ensureNoOpenChatResumeModalBeforeCandidateClick(client, {
238
571
  closeAttempts = 3
@@ -352,7 +685,7 @@ export function resolveChatDomFallbackWait({
352
685
  };
353
686
  }
354
687
 
355
- function isRecoverableCdpNodeError(error) {
688
+ function isRecoverableCdpNodeError(error) {
356
689
  return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
357
690
  .test(String(error?.message || error || ""));
358
691
  }
@@ -363,9 +696,9 @@ function isRecoverableLlmScreeningError(error) {
363
696
  }
364
697
 
365
698
  function createFailedLlmResult(error) {
366
- return {
367
- ok: false,
368
- passed: false,
699
+ return {
700
+ ok: false,
701
+ passed: false,
369
702
  reason: "",
370
703
  evidence: [],
371
704
  cot: "",
@@ -373,110 +706,114 @@ function createFailedLlmResult(error) {
373
706
  reasoning_content: "",
374
707
  raw_model_output: "",
375
708
  attempt_count: Number(error?.llm_attempt_count) || 0,
376
- fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
377
- llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
378
- error_code: error?.code || null,
379
- fatal: Boolean(isFatalLlmProviderError(error)),
380
- fatal_reason: error?.llm_fatal_reason || "",
381
- error: error?.message || String(error || "unknown"),
382
- screened_at: new Date().toISOString()
383
- };
384
- }
385
-
386
- function normalizeScreeningMode(value) {
387
- const normalized = String(value || "llm").trim().toLowerCase();
388
- if (["collect_cv", "collect-cv", "cv_collection", "request_cv", "request_resume"].includes(normalized)) {
389
- return "collect_cv";
390
- }
391
- return ["deterministic", "local", "local_scorer"].includes(normalized)
392
- ? "deterministic"
393
- : "llm";
394
- }
395
-
396
- function isCvAcquiredOrAvailable(detailResult = null, preActionState = null) {
397
- return Boolean(
398
- preActionState?.attachment_resume_enabled
399
- || detailResult?.cv_acquisition?.full_cv_evidence?.full_cv_acquired
400
- );
401
- }
402
-
403
- function isChatResumeRequestAvailable(preActionState = null) {
404
- return Boolean(preActionState?.ask_resume?.node_id && !preActionState.ask_resume.disabled);
405
- }
406
-
407
- function shouldSkipCvCollectionForDetailReason(reason = "") {
408
- const normalized = normalizeText(reason);
409
- return [
410
- "active_candidate_mismatch",
411
- "forbidden_top_level_resume_navigation",
412
- "online_resume_modal_did_not_open",
413
- "unsafe_online_resume_navigation_link"
414
- ].includes(normalized)
415
- || normalized.startsWith("recoverable_cdp_node_stale:")
416
- || normalized.startsWith("resume_modal_close_failed:");
417
- }
418
-
419
- export function createCvCollectionScreening(screeningCandidate, {
420
- detailResult = null,
421
- detailUnavailableReason = "",
422
- preActionState = null
423
- } = {}) {
424
- if (preActionState?.already_requested_resume) {
425
- return {
426
- status: "skip",
427
- passed: false,
428
- score: 0,
429
- reasons: ["resume_request_already_pending"],
430
- candidate: screeningCandidate
431
- };
432
- }
433
- if (isCvAcquiredOrAvailable(detailResult, preActionState)) {
434
- const reason = preActionState?.attachment_resume_enabled
435
- ? "attachment_resume_already_available"
436
- : "online_cv_already_available";
437
- return {
438
- status: "skip",
439
- passed: false,
440
- score: 0,
441
- reasons: [reason],
442
- candidate: screeningCandidate
443
- };
444
- }
445
- if (isChatResumeRequestAvailable(preActionState)) {
446
- const reason = detailUnavailableReason || "request_cv_available";
447
- return {
448
- status: "pass",
449
- passed: true,
450
- score: 100,
451
- reasons: [`collect_cv:${reason}`],
452
- candidate: screeningCandidate
453
- };
454
- }
455
- if (shouldSkipCvCollectionForDetailReason(detailUnavailableReason)) {
456
- return {
457
- status: "skip",
458
- passed: false,
459
- score: 0,
460
- reasons: [detailUnavailableReason],
461
- candidate: screeningCandidate
462
- };
463
- }
464
- const reason = detailUnavailableReason || "cv_collection_missing_online_cv";
465
- return {
466
- status: "pass",
467
- passed: true,
468
- score: 100,
469
- reasons: [`collect_cv:${reason}`],
470
- candidate: screeningCandidate
471
- };
472
- }
473
-
474
- export function shouldOpenOnlineResumeForChatDetail({
475
- collectCvOnly = false,
476
- detailResult = null
477
- } = {}) {
478
- return !collectCvOnly && !detailResult;
479
- }
709
+ fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
710
+ llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
711
+ error_code: error?.code || null,
712
+ fatal: Boolean(isFatalLlmProviderError(error)),
713
+ fatal_reason: error?.llm_fatal_reason || "",
714
+ error: error?.message || String(error || "unknown"),
715
+ screened_at: new Date().toISOString()
716
+ };
717
+ }
718
+
719
+ export function isRecoverableChatImageCaptureError(error) {
720
+ return isRecoverableImageCaptureWorkflowError(error);
721
+ }
722
+
723
+ function normalizeScreeningMode(value) {
724
+ const normalized = String(value || "llm").trim().toLowerCase();
725
+ if (["collect_cv", "collect-cv", "cv_collection", "request_cv", "request_resume"].includes(normalized)) {
726
+ return "collect_cv";
727
+ }
728
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
729
+ ? "deterministic"
730
+ : "llm";
731
+ }
732
+
733
+ function isCvAcquiredOrAvailable(detailResult = null, preActionState = null) {
734
+ return Boolean(
735
+ preActionState?.attachment_resume_enabled
736
+ || detailResult?.cv_acquisition?.full_cv_evidence?.full_cv_acquired
737
+ );
738
+ }
739
+
740
+ function isChatResumeRequestAvailable(preActionState = null) {
741
+ return Boolean(preActionState?.ask_resume?.node_id && !preActionState.ask_resume.disabled);
742
+ }
743
+
744
+ function shouldSkipCvCollectionForDetailReason(reason = "") {
745
+ const normalized = normalizeText(reason);
746
+ return [
747
+ "active_candidate_mismatch",
748
+ "forbidden_top_level_resume_navigation",
749
+ "online_resume_modal_did_not_open",
750
+ "unsafe_online_resume_navigation_link"
751
+ ].includes(normalized)
752
+ || normalized.startsWith("recoverable_cdp_node_stale:")
753
+ || normalized.startsWith("resume_modal_close_failed:");
754
+ }
755
+
756
+ export function createCvCollectionScreening(screeningCandidate, {
757
+ detailResult = null,
758
+ detailUnavailableReason = "",
759
+ preActionState = null
760
+ } = {}) {
761
+ if (preActionState?.already_requested_resume) {
762
+ return {
763
+ status: "skip",
764
+ passed: false,
765
+ score: 0,
766
+ reasons: ["resume_request_already_pending"],
767
+ candidate: screeningCandidate
768
+ };
769
+ }
770
+ if (isCvAcquiredOrAvailable(detailResult, preActionState)) {
771
+ const reason = preActionState?.attachment_resume_enabled
772
+ ? "attachment_resume_already_available"
773
+ : "online_cv_already_available";
774
+ return {
775
+ status: "skip",
776
+ passed: false,
777
+ score: 0,
778
+ reasons: [reason],
779
+ candidate: screeningCandidate
780
+ };
781
+ }
782
+ if (isChatResumeRequestAvailable(preActionState)) {
783
+ const reason = detailUnavailableReason || "request_cv_available";
784
+ return {
785
+ status: "pass",
786
+ passed: true,
787
+ score: 100,
788
+ reasons: [`collect_cv:${reason}`],
789
+ candidate: screeningCandidate
790
+ };
791
+ }
792
+ if (shouldSkipCvCollectionForDetailReason(detailUnavailableReason)) {
793
+ return {
794
+ status: "skip",
795
+ passed: false,
796
+ score: 0,
797
+ reasons: [detailUnavailableReason],
798
+ candidate: screeningCandidate
799
+ };
800
+ }
801
+ const reason = detailUnavailableReason || "cv_collection_missing_online_cv";
802
+ return {
803
+ status: "pass",
804
+ passed: true,
805
+ score: 100,
806
+ reasons: [`collect_cv:${reason}`],
807
+ candidate: screeningCandidate
808
+ };
809
+ }
810
+
811
+ export function shouldOpenOnlineResumeForChatDetail({
812
+ collectCvOnly = false,
813
+ detailResult = null
814
+ } = {}) {
815
+ return !collectCvOnly && !detailResult;
816
+ }
480
817
 
481
818
  function createMissingLlmConfigResult() {
482
819
  return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
@@ -498,81 +835,74 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
498
835
  };
499
836
  }
500
837
 
501
- function compactChatRuntimeError(error) {
502
- if (!error) return null;
503
- return {
504
- name: error.name || "Error",
505
- code: error.code || null,
506
- message: error.message || String(error),
507
- retryable: typeof error.retryable === "boolean" ? error.retryable : null,
508
- attempts: Array.isArray(error.attempts) ? error.attempts : null,
509
- close_result: error.close_result || null,
510
- selection_ready_state: error.selection_ready_state || null,
511
- page_state: error.page_state || null
512
- };
513
- }
514
-
515
- async function captureChatFinalFailureArtifact(client, {
516
- runControl,
517
- imageOutputDir = "",
518
- error = null
519
- } = {}) {
520
- if (!client || !imageOutputDir || !runControl?.runId) return null;
521
- const artifact = {
522
- schema_version: 1,
523
- kind: "chat_final_failure_page",
524
- captured_at: new Date().toISOString(),
525
- run_id: runControl.runId,
526
- error: compactChatRuntimeError(error),
527
- page_state: null,
528
- active_candidate_state: null,
529
- conversation_ready_state: null,
530
- screenshot: null,
531
- screenshot_error: null
532
- };
533
- try {
534
- artifact.page_state = await getChatTopLevelState(client);
535
- } catch (pageError) {
536
- artifact.page_state = {
537
- error: pageError?.message || String(pageError)
538
- };
539
- }
540
- try {
541
- artifact.active_candidate_state = await readChatActiveCandidateState(client);
542
- } catch (activeCandidateError) {
543
- artifact.active_candidate_state = {
544
- error: activeCandidateError?.message || String(activeCandidateError)
545
- };
546
- }
547
- try {
548
- artifact.conversation_ready_state = await readChatConversationReadyState(client);
549
- } catch (conversationError) {
550
- artifact.conversation_ready_state = {
551
- error: conversationError?.message || String(conversationError)
552
- };
553
- }
554
- try {
555
- artifact.screenshot = await captureViewportScreenshot(client, {
556
- filePath: imageEvidenceFilePath({
557
- imageOutputDir,
558
- domain: "chat-final-failure",
559
- runId: runControl.runId,
560
- index: 0,
561
- extension: "jpg"
562
- }),
563
- format: "jpeg",
564
- quality: 72,
565
- metadata: {
566
- domain: "chat",
567
- run_id: runControl.runId,
568
- reason: "final_failure"
569
- }
570
- });
571
- } catch (screenshotError) {
572
- artifact.screenshot_error = screenshotError?.message || String(screenshotError);
573
- }
574
- return artifact;
575
- }
838
+ function compactChatRuntimeError(error) {
839
+ if (!error) return null;
840
+ return {
841
+ name: error.name || "Error",
842
+ code: error.code || null,
843
+ message: error.message || String(error),
844
+ retryable: typeof error.retryable === "boolean" ? error.retryable : null,
845
+ attempts: Array.isArray(error.attempts) ? error.attempts : null,
846
+ close_result: error.close_result || null,
847
+ selection_ready_state: error.selection_ready_state || null,
848
+ page_state: error.page_state || null
849
+ };
850
+ }
851
+
852
+ async function captureChatFinalFailureArtifact(client, {
853
+ runControl,
854
+ imageOutputDir = "",
855
+ error = null
856
+ } = {}) {
857
+ if (!client || !imageOutputDir || !runControl?.runId) return null;
858
+ const artifact = {
859
+ schema_version: 1,
860
+ kind: "chat_final_failure_page",
861
+ captured_at: new Date().toISOString(),
862
+ run_id: runControl.runId,
863
+ error: compactChatRuntimeError(error),
864
+ page_state: null,
865
+ active_candidate_state: null,
866
+ conversation_ready_state: null,
867
+ screenshot: null,
868
+ screenshot_error: null
869
+ };
870
+ try {
871
+ artifact.page_state = await getChatTopLevelState(client);
872
+ } catch (pageError) {
873
+ artifact.page_state = {
874
+ error: pageError?.message || String(pageError)
875
+ };
876
+ }
877
+ try {
878
+ artifact.active_candidate_state = await readChatActiveCandidateState(client);
879
+ } catch (activeCandidateError) {
880
+ artifact.active_candidate_state = {
881
+ error: activeCandidateError?.message || String(activeCandidateError)
882
+ };
883
+ }
884
+ try {
885
+ artifact.conversation_ready_state = await readChatConversationReadyState(client);
886
+ } catch (conversationError) {
887
+ artifact.conversation_ready_state = {
888
+ error: conversationError?.message || String(conversationError)
889
+ };
890
+ }
891
+ try {
892
+ artifact.screenshot = await captureViewportScreenshot(client, {
893
+ format: "jpeg",
894
+ quality: 72,
895
+ metadata: {
896
+ domain: "chat",
897
+ run_id: runControl.runId,
898
+ reason: "final_failure"
899
+ }
900
+ });
901
+ } catch (screenshotError) {
902
+ artifact.screenshot_error = screenshotError?.message || String(screenshotError);
903
+ }
904
+ return artifact;
905
+ }
576
906
 
577
907
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
578
908
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
@@ -677,8 +1007,13 @@ function isFullCvNetworkProfile(profileResult = {}) {
677
1007
  return false;
678
1008
  }
679
1009
 
680
- function hasUsableImageEvidence(imageEvidence = null) {
681
- if (!imageEvidence || imageEvidence.ok === false) return false;
1010
+ function hasUsableImageEvidence(imageEvidence = null) {
1011
+ if (
1012
+ !imageEvidence
1013
+ || imageEvidence.ok === false
1014
+ || imageEvidence.coverage_complete !== true
1015
+ || isIncompleteImageEvidence(imageEvidence)
1016
+ ) return false;
682
1017
  return Boolean(
683
1018
  (Array.isArray(imageEvidence.llm_file_paths) && imageEvidence.llm_file_paths.length)
684
1019
  || (Array.isArray(imageEvidence.file_paths) && imageEvidence.file_paths.length)
@@ -689,7 +1024,7 @@ function hasUsableImageEvidence(imageEvidence = null) {
689
1024
  );
690
1025
  }
691
1026
 
692
- export function summarizeChatFullCvEvidence({
1027
+ export function summarizeChatFullCvEvidence({
693
1028
  detailResult = null,
694
1029
  contentWait = null,
695
1030
  imageEvidence = null
@@ -740,15 +1075,15 @@ async function resolveFreshChatCardNodeId(client, {
740
1075
  return freshNodeId || fallbackNodeId;
741
1076
  }
742
1077
 
743
- async function selectFreshChatCandidate(client, {
744
- cardNodeId,
745
- candidate,
746
- timeoutMs,
747
- settleMs = 1200,
748
- onlineResumeProbe = true
749
- } = {}) {
750
- let lastError = null;
751
- for (let attempt = 0; attempt < 3; attempt += 1) {
1078
+ async function selectFreshChatCandidate(client, {
1079
+ cardNodeId,
1080
+ candidate,
1081
+ timeoutMs,
1082
+ settleMs = 1200,
1083
+ onlineResumeProbe = true
1084
+ } = {}) {
1085
+ let lastError = null;
1086
+ for (let attempt = 0; attempt < 3; attempt += 1) {
752
1087
  const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
753
1088
  const rootState = await getChatRoots(client);
754
1089
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
@@ -759,58 +1094,78 @@ async function selectFreshChatCandidate(client, {
759
1094
  try {
760
1095
  await scrollNodeIntoView(client, freshNodeId);
761
1096
  await sleep(250);
762
- const box = await getNodeBox(client, freshNodeId);
763
- await clickPoint(client, box.center.x, box.center.y);
764
- if (settleMs > 0) await sleep(settleMs);
765
- const ready = onlineResumeProbe
766
- ? await waitForChatOnlineResumeButton(client, {
767
- timeoutMs,
768
- expectedCandidateId: candidate?.id || ""
769
- })
770
- : await readSelectedChatCandidateState(client, candidate);
771
- return {
772
- card_box: box,
773
- ready,
774
- card_node_id: freshNodeId,
775
- refreshed_node: freshNodeId !== cardNodeId,
776
- modal_guard: modalGuard,
777
- attempt: attempt + 1
778
- };
1097
+ const box = await getNodeBox(client, freshNodeId);
1098
+ await clickPoint(client, box.center.x, box.center.y);
1099
+ if (settleMs > 0) await sleep(settleMs);
1100
+ const ready = onlineResumeProbe
1101
+ ? await waitForChatOnlineResumeButton(client, {
1102
+ timeoutMs,
1103
+ expectedCandidateId: candidate?.id || ""
1104
+ })
1105
+ : await readSelectedChatCandidateState(client, candidate);
1106
+ const selected = {
1107
+ card_box: box,
1108
+ ready,
1109
+ card_node_id: freshNodeId,
1110
+ refreshed_node: freshNodeId !== cardNodeId,
1111
+ modal_guard: modalGuard,
1112
+ attempt: attempt + 1
1113
+ };
1114
+ if (!ready?.forbidden_top_level_navigation) {
1115
+ requireExactChatCandidateSelection(selected, candidate);
1116
+ }
1117
+ return selected;
779
1118
  } catch (error) {
780
1119
  lastError = error;
781
1120
  if (!isRecoverableCdpNodeError(error)) throw error;
782
1121
  await sleep(350);
783
1122
  }
784
1123
  }
785
- throw lastError || new Error("Chat candidate selection failed");
786
- }
787
-
788
- async function readSelectedChatCandidateState(client, candidate = null) {
789
- const topLevelState = await getChatTopLevelState(client);
790
- if (topLevelState.is_forbidden_resume_top_level) {
791
- return {
792
- forbidden_top_level_navigation: true,
793
- top_level_state: topLevelState
794
- };
795
- }
796
- const activeState = await readChatActiveCandidateState(client);
797
- const expectedId = normalizeText(candidate?.id || "");
798
- const activeCandidateId = normalizeText(activeState?.active_candidate?.candidate_id || "");
799
- const candidateSelectionVerified = expectedId
800
- ? activeCandidateId === expectedId
801
- : undefined;
802
- return {
803
- ok: !expectedId || candidateSelectionVerified === true,
804
- reason: expectedId && candidateSelectionVerified !== true
805
- ? "active_candidate_mismatch"
806
- : "online_resume_probe_skipped",
807
- roots: activeState.roots,
808
- activeCandidate: activeState.active_candidate,
809
- expected_candidate_id: expectedId || null,
810
- active_candidate_id: activeCandidateId || null,
811
- candidate_selection_verified: candidateSelectionVerified
812
- };
813
- }
1124
+ throw lastError || new Error("Chat candidate selection failed");
1125
+ }
1126
+
1127
+ async function readSelectedChatCandidateState(client, candidate = null) {
1128
+ const topLevelState = await getChatTopLevelState(client);
1129
+ if (topLevelState.is_security_verification) {
1130
+ throw makeBossSecurityVerificationRequiredError(
1131
+ topLevelState,
1132
+ "chat_candidate_selection"
1133
+ );
1134
+ }
1135
+ if (topLevelState.is_forbidden_resume_top_level) {
1136
+ return {
1137
+ forbidden_top_level_navigation: true,
1138
+ top_level_state: topLevelState
1139
+ };
1140
+ }
1141
+ const activeState = await readChatActiveCandidateState(client);
1142
+ const expectedId = normalizeText(candidate?.id || "");
1143
+ const activeCandidateId = normalizeText(activeState?.active_candidate?.candidate_id || "");
1144
+ const candidateSelectionVerified = expectedId
1145
+ ? activeCandidateId === expectedId
1146
+ : undefined;
1147
+ return {
1148
+ ok: !expectedId || candidateSelectionVerified === true,
1149
+ reason: expectedId && candidateSelectionVerified !== true
1150
+ ? "active_candidate_mismatch"
1151
+ : "online_resume_probe_skipped",
1152
+ roots: activeState.roots,
1153
+ activeCandidate: activeState.active_candidate,
1154
+ expected_candidate_id: expectedId || null,
1155
+ active_candidate_id: activeCandidateId || null,
1156
+ candidate_selection_verified: candidateSelectionVerified
1157
+ };
1158
+ }
1159
+
1160
+ export function hasScreenableChatFullCvEvidence(fullCvEvidence = null, imageEvidence = null) {
1161
+ if (fullCvEvidence?.source === "image") {
1162
+ return Boolean(fullCvEvidence.full_cv_acquired && hasUsableImageEvidence(imageEvidence));
1163
+ }
1164
+ return Boolean(
1165
+ fullCvEvidence?.full_cv_acquired
1166
+ && !isIncompleteImageEvidence(imageEvidence)
1167
+ );
1168
+ }
814
1169
 
815
1170
  function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
816
1171
  if (detailSource !== "network" && detailSource !== "cascade") return [];
@@ -936,37 +1291,55 @@ export async function runChatWorkflow({
936
1291
  listWheelDeltaY = 850,
937
1292
  listSettleMs = 2200,
938
1293
  listFallbackPoint = null,
939
- imageOutputDir = "",
940
- humanRestEnabled = false,
941
- humanBehavior = null
942
- } = {}, runControl) {
1294
+ imageOutputDir = "",
1295
+ humanRestEnabled = false,
1296
+ humanBehavior = null,
1297
+ actionJournal = null,
1298
+ actionJournalScope = "boss-chat:default"
1299
+ } = {}, runControl) {
943
1300
  if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
944
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
945
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
946
- });
947
- const normalizedDetailSource = normalizeDetailSource(detailSource);
948
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
949
- const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
950
- const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
951
- const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
952
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
953
- configureHumanInteraction(client, {
954
- enabled: effectiveHumanBehavior.enabled,
955
- clickMovementEnabled: effectiveHumanBehavior.clickMovement,
1301
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1302
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1303
+ });
1304
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
1305
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
1306
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
1307
+ const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
1308
+ const collectCvProcessingFloorEnabled = collectCvOnly;
1309
+ const collectCvPerCandidateRestEnabled = false;
1310
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1311
+ const effectiveActionJournal = requestResumeForPassed
1312
+ ? actionJournal || createChatActionJournal()
1313
+ : actionJournal;
1314
+ configureHumanInteraction(client, {
1315
+ enabled: effectiveHumanBehavior.enabled,
1316
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
956
1317
  textEntryEnabled: effectiveHumanBehavior.textEntry,
957
1318
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
958
1319
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
959
1320
  });
960
- const humanRestController = createHumanRestController({
961
- enabled: effectiveHumanRestEnabled,
962
- shortRestEnabled: effectiveHumanBehavior.shortRest,
963
- batchRestEnabled: effectiveHumanBehavior.batchRest,
964
- restLevel: effectiveHumanBehavior.restLevel,
965
- perCandidateRestEnabled: collectCvPerCandidateRestEnabled,
966
- perCandidateRestMinMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS,
967
- perCandidateRestMaxMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS
968
- });
969
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1321
+ const humanRestController = createHumanRestController({
1322
+ enabled: effectiveHumanRestEnabled,
1323
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
1324
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
1325
+ restLevel: effectiveHumanBehavior.restLevel
1326
+ });
1327
+ const collectCvProcessingFloorState = {
1328
+ enabled: collectCvProcessingFloorEnabled,
1329
+ min_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS : null,
1330
+ max_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS : null,
1331
+ count: 0,
1332
+ total_delay_requested_ms: 0,
1333
+ total_delay_elapsed_ms: 0,
1334
+ last: null
1335
+ };
1336
+ const collectCvProcessingFloorSnapshot = () => ({
1337
+ ...collectCvProcessingFloorState,
1338
+ last: collectCvProcessingFloorState.last
1339
+ ? { ...collectCvProcessingFloorState.last }
1340
+ : null
1341
+ });
1342
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
970
1343
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
971
1344
  ? Number(targetPassCount)
972
1345
  : null;
@@ -1006,12 +1379,13 @@ export async function runChatWorkflow({
1006
1379
  let requestedCount = 0;
1007
1380
  let requestSatisfiedCount = 0;
1008
1381
  let requestSkippedCount = 0;
1009
- let contextSetup = {};
1010
- let contextRecoveryAttempts = 0;
1011
- const candidateRecoveryCounts = new Map();
1012
- let lastHumanEvent = null;
1013
-
1014
- function recordHumanEvent(event = null) {
1382
+ let contextSetup = {};
1383
+ let contextRecoveryAttempts = 0;
1384
+ const candidateRecoveryCounts = new Map();
1385
+ const imageCaptureWorkflowRetries = createImageCaptureWorkflowRetryTracker();
1386
+ let lastHumanEvent = null;
1387
+
1388
+ function recordHumanEvent(event = null) {
1015
1389
  if (!event) return lastHumanEvent;
1016
1390
  lastHumanEvent = {
1017
1391
  at: new Date().toISOString(),
@@ -1037,9 +1411,15 @@ export async function runChatWorkflow({
1037
1411
  });
1038
1412
  }
1039
1413
 
1040
- runControl.setPhase("chat:cleanup");
1041
- let initialTopLevelState = await getChatTopLevelState(client);
1042
- if (!initialTopLevelState.is_chat_shell) {
1414
+ runControl.setPhase("chat:cleanup");
1415
+ let initialTopLevelState = await getChatTopLevelState(client);
1416
+ if (initialTopLevelState.is_security_verification) {
1417
+ throw makeBossSecurityVerificationRequiredError(
1418
+ initialTopLevelState,
1419
+ "chat_run_setup"
1420
+ );
1421
+ }
1422
+ if (!initialTopLevelState.is_chat_shell) {
1043
1423
  const recovery = await recoverChatShell(client, {
1044
1424
  targetUrl,
1045
1425
  timeoutMs: readyTimeoutMs,
@@ -1075,13 +1455,13 @@ export async function runChatWorkflow({
1075
1455
  ...setup.contextSetup,
1076
1456
  initial_top_level_state: initialTopLevelState
1077
1457
  };
1078
- runControl.checkpoint({
1079
- chat_context: contextSetup
1080
- });
1081
-
1082
- async function recoverAndReapplyChatContext(reason, error = null, {
1083
- forceRefresh = false
1084
- } = {}) {
1458
+ runControl.checkpoint({
1459
+ chat_context: contextSetup
1460
+ });
1461
+
1462
+ async function recoverAndReapplyChatContext(reason, error = null, {
1463
+ forceRefresh = false
1464
+ } = {}) {
1085
1465
  runControl.setPhase("chat:recover_shell");
1086
1466
  contextRecoveryAttempts += 1;
1087
1467
  const shellRecovery = await recoverChatShell(client, {
@@ -1183,14 +1563,20 @@ export async function runChatWorkflow({
1183
1563
  viewport_checks: viewportGuard.getStats().checks,
1184
1564
  viewport_recoveries: viewportGuard.getStats().recoveries,
1185
1565
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1186
- human_behavior_profile: effectiveHumanBehavior.profile,
1187
- human_rest_level: effectiveHumanBehavior.restLevel,
1188
- human_rest_enabled: effectiveHumanRestEnabled,
1189
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1190
- human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1191
- human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1192
- human_rest_count: humanRestController.getState().rest_count,
1193
- human_rest_ms: humanRestController.getState().total_rest_ms,
1566
+ human_behavior_profile: effectiveHumanBehavior.profile,
1567
+ human_rest_level: effectiveHumanBehavior.restLevel,
1568
+ human_rest_enabled: effectiveHumanRestEnabled,
1569
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1570
+ human_rest_per_candidate_min_ms: null,
1571
+ human_rest_per_candidate_max_ms: null,
1572
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
1573
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorState.min_ms,
1574
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorState.max_ms,
1575
+ collect_cv_processing_floor_count: collectCvProcessingFloorState.count,
1576
+ collect_cv_processing_floor_delay_requested_ms: collectCvProcessingFloorState.total_delay_requested_ms,
1577
+ collect_cv_processing_floor_delay_ms: collectCvProcessingFloorState.total_delay_elapsed_ms,
1578
+ human_rest_count: humanRestController.getState().rest_count,
1579
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1194
1580
  last_human_event: lastHumanEvent
1195
1581
  });
1196
1582
  runControl.setPhase("chat:done");
@@ -1208,16 +1594,17 @@ export async function runChatWorkflow({
1208
1594
  viewport_health: {
1209
1595
  stats: viewportGuard.getStats(),
1210
1596
  events: viewportGuard.getEvents()
1211
- },
1212
- human_behavior: effectiveHumanBehavior,
1213
- human_rest: humanRestController.getState(),
1214
- last_human_event: lastHumanEvent,
1597
+ },
1598
+ human_behavior: effectiveHumanBehavior,
1599
+ human_rest: humanRestController.getState(),
1600
+ collect_cv_processing_floor: collectCvProcessingFloorSnapshot(),
1601
+ last_human_event: lastHumanEvent,
1215
1602
  list_end_reason: listEndReason,
1216
1603
  target_pass_count: passTarget,
1217
- process_until_list_end: Boolean(processUntilListEnd),
1218
- processed_limit: processedLimit,
1219
- detail_source: normalizedDetailSource,
1220
- processed: 0,
1604
+ process_until_list_end: Boolean(processUntilListEnd),
1605
+ processed_limit: processedLimit,
1606
+ detail_source: normalizedDetailSource,
1607
+ processed: 0,
1221
1608
  screened: 0,
1222
1609
  detail_opened: 0,
1223
1610
  llm_screened: 0,
@@ -1248,20 +1635,27 @@ export async function runChatWorkflow({
1248
1635
  screening_mode: normalizedScreeningMode,
1249
1636
  unique_seen: compactInfiniteListState(listState).seen_count,
1250
1637
  scroll_count: 0,
1251
- context_recoveries: contextRecoveryAttempts,
1252
- viewport_checks: viewportGuard.getStats().checks,
1253
- viewport_recoveries: viewportGuard.getStats().recoveries,
1254
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1255
- human_behavior_profile: effectiveHumanBehavior.profile,
1256
- human_rest_level: effectiveHumanBehavior.restLevel,
1257
- human_rest_enabled: effectiveHumanRestEnabled,
1258
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1259
- human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1260
- human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1261
- human_rest_count: humanRestController.getState().rest_count,
1262
- human_rest_ms: humanRestController.getState().total_rest_ms,
1263
- last_human_event: lastHumanEvent
1264
- });
1638
+ context_recoveries: contextRecoveryAttempts,
1639
+ viewport_checks: viewportGuard.getStats().checks,
1640
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1641
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1642
+ human_behavior_profile: effectiveHumanBehavior.profile,
1643
+ human_rest_level: effectiveHumanBehavior.restLevel,
1644
+ human_rest_enabled: effectiveHumanRestEnabled,
1645
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1646
+ human_rest_per_candidate_min_ms: null,
1647
+ human_rest_per_candidate_max_ms: null,
1648
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
1649
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorState.min_ms,
1650
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorState.max_ms,
1651
+ collect_cv_processing_floor_count: collectCvProcessingFloorState.count,
1652
+ collect_cv_processing_floor_delay_requested_ms: collectCvProcessingFloorState.total_delay_requested_ms,
1653
+ collect_cv_processing_floor_delay_ms: collectCvProcessingFloorState.total_delay_elapsed_ms,
1654
+ collect_cv_processing_floor_current: null,
1655
+ human_rest_count: humanRestController.getState().rest_count,
1656
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1657
+ last_human_event: lastHumanEvent
1658
+ });
1265
1659
 
1266
1660
  while (
1267
1661
  results.length < processedLimit
@@ -1275,9 +1669,15 @@ export async function runChatWorkflow({
1275
1669
  await runControl.waitIfPaused();
1276
1670
  runControl.throwIfCanceled();
1277
1671
  runControl.setPhase("chat:candidate");
1278
- rootState = await ensureChatViewport(rootState, "candidate_loop");
1279
- const loopTopLevelState = await getChatTopLevelState(client);
1280
- if (!loopTopLevelState.is_chat_shell) {
1672
+ rootState = await ensureChatViewport(await getChatRoots(client), "candidate_loop");
1673
+ const loopTopLevelState = await getChatTopLevelState(client);
1674
+ if (loopTopLevelState.is_security_verification) {
1675
+ throw makeBossSecurityVerificationRequiredError(
1676
+ loopTopLevelState,
1677
+ "chat_candidate_loop"
1678
+ );
1679
+ }
1680
+ if (!loopTopLevelState.is_chat_shell) {
1281
1681
  await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
1282
1682
  message: `Unexpected chat top-level URL: ${loopTopLevelState.url}`
1283
1683
  });
@@ -1370,9 +1770,15 @@ export async function runChatWorkflow({
1370
1770
  maxTextScanNodes: 500
1371
1771
  })
1372
1772
  }));
1373
- if (!nextCandidateResult.ok) {
1374
- const endTopLevelState = await getChatTopLevelState(client);
1375
- if (!endTopLevelState.is_chat_shell) {
1773
+ if (!nextCandidateResult.ok) {
1774
+ const endTopLevelState = await getChatTopLevelState(client);
1775
+ if (endTopLevelState.is_security_verification) {
1776
+ throw makeBossSecurityVerificationRequiredError(
1777
+ endTopLevelState,
1778
+ "chat_candidate_list_end"
1779
+ );
1780
+ }
1781
+ if (!endTopLevelState.is_chat_shell) {
1376
1782
  await recoverAndReapplyChatContext("candidate_list_end_non_chat_shell", {
1377
1783
  message: `Unexpected chat top-level URL at list end: ${endTopLevelState.url}`
1378
1784
  });
@@ -1392,15 +1798,21 @@ export async function runChatWorkflow({
1392
1798
  }
1393
1799
 
1394
1800
  const index = results.length;
1395
- const cardNodeId = nextCandidateResult.item.node_id;
1396
- let effectiveCardNodeId = cardNodeId;
1397
- const candidateKey = nextCandidateResult.item.key;
1398
- const cardCandidate = nextCandidateResult.item.candidate;
1399
-
1400
- let screeningCandidate = cardCandidate;
1401
- let detailResult = null;
1402
- let preActionState = null;
1403
- let detailUnavailableReason = "";
1801
+ const cardNodeId = nextCandidateResult.item.node_id;
1802
+ let effectiveCardNodeId = cardNodeId;
1803
+ const candidateKey = nextCandidateResult.item.key;
1804
+ const cardCandidate = nextCandidateResult.item.candidate;
1805
+ const candidateProcessingStarted = Date.now();
1806
+ const candidateProcessingFloorTargetMs = collectCvProcessingFloorEnabled
1807
+ ? sampleChatCollectCvProcessingFloorMs()
1808
+ : 0;
1809
+
1810
+ let screeningCandidate = cardCandidate;
1811
+ let detailResult = null;
1812
+ let preActionState = null;
1813
+ let selectionReadyState = null;
1814
+ let preScreeningActionReconciliation = null;
1815
+ let detailUnavailableReason = "";
1404
1816
  if (index < detailCountLimit) {
1405
1817
  let detailStep = "start";
1406
1818
  const checkpointInProgressCandidate = (patch = {}) => runControl.checkpoint({
@@ -1415,45 +1827,75 @@ export async function runChatWorkflow({
1415
1827
  }
1416
1828
  });
1417
1829
  try {
1418
- await runControl.waitIfPaused();
1419
- runControl.throwIfCanceled();
1420
- runControl.setPhase("chat:detail");
1421
- rootState = await ensureChatViewport(rootState, "detail");
1422
- checkpointInProgressCandidate({ event: "detail_start" });
1830
+ await runControl.waitIfPaused();
1831
+ runControl.throwIfCanceled();
1832
+ runControl.setPhase("chat:detail");
1833
+ checkpointInProgressCandidate({ event: "detail_start" });
1834
+ rootState = await ensureChatViewport(rootState, "detail");
1423
1835
 
1424
1836
  detailStep = "select_candidate";
1425
1837
  networkRecorder.clear();
1426
1838
  await maybeHumanActionCooldown("before_detail_open", timings);
1427
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1428
- cardNodeId,
1429
- candidate: cardCandidate,
1430
- timeoutMs: onlineResumeButtonTimeoutMs,
1431
- onlineResumeProbe: !collectCvOnly
1432
- }));
1433
- if (selected.ready?.forbidden_top_level_navigation) {
1434
- throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1435
- }
1436
- effectiveCardNodeId = selected.card_node_id || cardNodeId;
1437
- const selectionNetworkEvents = networkRecorder.events.slice();
1438
- try {
1439
- preActionState = await readChatConversationReadyState(client);
1440
- } catch (error) {
1839
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1840
+ cardNodeId,
1841
+ candidate: cardCandidate,
1842
+ timeoutMs: onlineResumeButtonTimeoutMs,
1843
+ onlineResumeProbe: !collectCvOnly
1844
+ }));
1845
+ if (selected.ready?.forbidden_top_level_navigation) {
1846
+ throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1847
+ }
1848
+ selectionReadyState = selected.ready || null;
1849
+ effectiveCardNodeId = selected.card_node_id || cardNodeId;
1850
+ checkpointInProgressCandidate({
1851
+ event: "candidate_selection_verified",
1852
+ selection_ready_state: compactChatCandidateSelectionReadyState(selectionReadyState)
1853
+ });
1854
+ const selectionNetworkEvents = networkRecorder.events.slice();
1855
+ try {
1856
+ preActionState = await readChatConversationReadyState(client);
1857
+ } catch (error) {
1441
1858
  preActionState = {
1442
- error: error?.message || String(error)
1443
- };
1444
- }
1445
- const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
1859
+ error: error?.message || String(error)
1860
+ };
1861
+ }
1862
+ if (
1863
+ requestResumeForPassed
1864
+ && !dryRunRequestCv
1865
+ && effectiveActionJournal
1866
+ && (preActionState?.already_requested_resume || preActionState?.attachment_resume_enabled)
1867
+ ) {
1868
+ const candidateId = normalizeText(cardCandidate.id || "");
1869
+ const actionJournalRecord = candidateId
1870
+ ? effectiveActionJournal.read({
1871
+ scope: actionJournalScope,
1872
+ candidateId
1873
+ })
1874
+ : null;
1875
+ preScreeningActionReconciliation = reconcileChatRequestJournalFromExactExistingState({
1876
+ actionJournal: effectiveActionJournal,
1877
+ actionJournalScope,
1878
+ actionJournalRecord,
1879
+ candidateId,
1880
+ selectionReadyState: selected.ready,
1881
+ readyState: preActionState,
1882
+ runId: runControl.runId,
1883
+ greeting: greetingText,
1884
+ checkpointCritical: (patch) => runControl.checkpointCritical(patch)
1885
+ });
1886
+ }
1887
+ const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
1446
1888
  if (preDetailSkipReason) {
1447
1889
  detailUnavailableReason = preDetailSkipReason;
1448
1890
  detailResult = createSkippedDetailResult(cardCandidate, preDetailSkipReason);
1449
1891
  detailResult.cv_acquisition.pre_detail_state = preActionState;
1450
1892
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
1451
1893
  }
1452
- if (!selected.ready?.ok) {
1453
- if (detailResult) {
1454
- // Already classified by the pre-detail conversation state.
1455
- } else if (selected.ready?.reason === "active_candidate_mismatch") {
1456
- throw makeChatCandidateSelectionMismatchError(selected, cardCandidate);
1894
+ if (!selected.ready?.ok) {
1895
+ if (detailResult) {
1896
+ // Already classified by the pre-detail conversation state.
1897
+ } else if (selected.ready?.reason === "active_candidate_mismatch") {
1898
+ throw makeChatCandidateSelectionMismatchError(selected, cardCandidate);
1457
1899
  } else {
1458
1900
  detailStep = "read_conversation_ready_state";
1459
1901
  if (preActionState.attachment_resume_enabled) {
@@ -1463,18 +1905,18 @@ export async function runChatWorkflow({
1463
1905
  } else {
1464
1906
  detailUnavailableReason = "online_resume_button_unavailable";
1465
1907
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1466
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1467
- }
1468
- }
1469
- }
1470
- if (collectCvOnly && !detailResult) {
1471
- detailUnavailableReason = preActionState?.has_online_resume
1472
- ? "collect_cv_request_candidate"
1473
- : "collect_cv_missing_online_resume";
1474
- }
1475
-
1476
- if (shouldOpenOnlineResumeForChatDetail({ collectCvOnly, detailResult })) {
1477
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1908
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1909
+ }
1910
+ }
1911
+ }
1912
+ if (collectCvOnly && !detailResult) {
1913
+ detailUnavailableReason = preActionState?.has_online_resume
1914
+ ? "collect_cv_request_candidate"
1915
+ : "collect_cv_missing_online_resume";
1916
+ }
1917
+
1918
+ if (shouldOpenOnlineResumeForChatDetail({ collectCvOnly, detailResult })) {
1919
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1478
1920
  let networkWait = null;
1479
1921
  let contentWait = {
1480
1922
  ok: false,
@@ -1665,57 +2107,212 @@ export async function runChatWorkflow({
1665
2107
  timeoutMs: 6000,
1666
2108
  intervalMs: 250
1667
2109
  });
1668
- captureTarget = captureTargetWait.target || null;
1669
- const captureNodeId = captureTarget?.node_id || null;
1670
- if (captureNodeId) {
1671
- detailStep = "capture_image_fallback";
1672
- imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
1673
- filePath: imageEvidenceFilePath({
1674
- imageOutputDir,
1675
- domain: "chat",
1676
- runId: runControl?.runId,
1677
- index,
1678
- extension: "jpg"
1679
- }),
1680
- format: "jpeg",
1681
- quality: 72,
1682
- optimize: true,
1683
- resizeMaxWidth: 1100,
1684
- captureViewport: false,
1685
- padding: 0,
1686
- maxScreenshots: maxImagePages,
1687
- wheelDeltaY: imageWheelDeltaY,
1688
- settleMs: 350,
1689
- scrollMethod: "dom-anchor-fallback-input",
1690
- scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1691
- stepTimeoutMs: 45000,
1692
- totalTimeoutMs: 90000,
1693
- duplicateStopCount: 1,
1694
- skipDuplicateScreenshots: true,
1695
- composeForLlm: true,
1696
- llmPagesPerImage: 3,
1697
- llmResizeMaxWidth: 1100,
1698
- llmQuality: 72,
1699
- stopBoundarySelector: CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR,
1700
- stopBoundaryTextPatterns: CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT,
1701
- stopBoundaryMaxProbeNodes: 360,
1702
- stopBoundaryTopPadding: 10,
1703
- stopBoundaryMinCaptureHeight: 180,
1704
- metadata: {
1705
- domain: "chat",
1706
- capture_mode: "scroll_sequence",
1707
- capture_scope: "resume_modal_clip",
1708
- acquisition_reason: normalizedDetailSource === "image"
1709
- ? "forced_image"
1710
- : "network_miss_image_fallback",
1711
- run_candidate_index: index,
1712
- candidate_key: candidateKey,
1713
- capture_target: captureTarget,
1714
- capture_target_wait: captureTargetWait
1715
- }
1716
- }));
1717
- source = "image";
1718
- fullCvEvidence = summarizeChatFullCvEvidence({
2110
+ captureTarget = captureTargetWait.target || null;
2111
+ const captureNodeId = captureTarget?.node_id || null;
2112
+ if (captureNodeId) {
2113
+ const imageEvidencePath = imageEvidenceFilePath({
2114
+ imageOutputDir,
2115
+ domain: "chat",
2116
+ runId: runControl?.runId,
2117
+ index,
2118
+ extension: "jpg"
2119
+ });
2120
+ const captureImageForTarget = (target, targetWait, resumeCheckpoint = null) => captureScrolledNodeScreenshots(
2121
+ client,
2122
+ target.node_id,
2123
+ {
2124
+ filePath: imageEvidencePath,
2125
+ format: "jpeg",
2126
+ quality: 72,
2127
+ optimize: true,
2128
+ resizeMaxWidth: 1100,
2129
+ captureViewport: false,
2130
+ iframeOwnerNodeId: target?.iframe_node_id || null,
2131
+ padding: 0,
2132
+ maxScreenshots: maxImagePages,
2133
+ wheelDeltaY: imageWheelDeltaY,
2134
+ settleMs: 350,
2135
+ scrollMethod: "dom-anchor-fallback-input",
2136
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
2137
+ stepTimeoutMs: 45000,
2138
+ totalTimeoutMs: 90000,
2139
+ duplicateStopCount: 2,
2140
+ skipDuplicateScreenshots: true,
2141
+ requireTerminalProof: true,
2142
+ composeForLlm: true,
2143
+ llmPagesPerImage: 3,
2144
+ llmResizeMaxWidth: 1100,
2145
+ llmQuality: 72,
2146
+ stopBoundarySelector: CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR,
2147
+ stopBoundaryTextPatterns: CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT,
2148
+ stopBoundaryMaxProbeNodes: 360,
2149
+ stopBoundaryTopPadding: 10,
2150
+ stopBoundaryMinCaptureHeight: 180,
2151
+ resumeCheckpoint,
2152
+ metadata: {
2153
+ domain: "chat",
2154
+ capture_mode: "scroll_sequence",
2155
+ capture_scope: "resume_modal_clip",
2156
+ acquisition_reason: normalizedDetailSource === "image"
2157
+ ? "forced_image"
2158
+ : "network_miss_image_fallback",
2159
+ run_candidate_index: index,
2160
+ candidate_key: candidateKey,
2161
+ capture_target: target,
2162
+ capture_target_wait: targetWait,
2163
+ resumed_from_checkpoint: Boolean(resumeCheckpoint)
2164
+ }
2165
+ }
2166
+ );
2167
+ detailStep = "capture_image_fallback";
2168
+ try {
2169
+ imageEvidence = await measureTiming(
2170
+ timings,
2171
+ "screenshot_capture_ms",
2172
+ () => captureImageForTarget(captureTarget, captureTargetWait)
2173
+ );
2174
+ imageEvidence = requireCompleteImageEvidence(imageEvidence, {
2175
+ code: "IMAGE_CAPTURE_EVIDENCE_MISSING",
2176
+ message: "Chat CV capture returned no complete persisted evidence",
2177
+ metadata: { domain: "chat", candidate_key: candidateKey }
2178
+ });
2179
+ } catch (error) {
2180
+ if (!isRecoverableChatImageCaptureError(error)) throw error;
2181
+ const retryReservation = imageCaptureWorkflowRetries.consume(candidateKey);
2182
+ const resumeCheckpoint = retryReservation.allowed
2183
+ ? imageCaptureResumeCheckpoint(error)
2184
+ : null;
2185
+ timings.image_capture_workflow_retry = retryReservation;
2186
+ if (!resumeCheckpoint) {
2187
+ imageEvidence = createRequiredImageEvidenceFailure({
2188
+ code: error?.code || "IMAGE_CAPTURE_CHECKPOINT_UNAVAILABLE",
2189
+ message: error?.message || "Chat CV capture failed without a resumable checkpoint",
2190
+ metadata: {
2191
+ domain: "chat",
2192
+ candidate_key: candidateKey,
2193
+ image_capture_workflow_retry: retryReservation
2194
+ }
2195
+ });
2196
+ timings.image_capture_resume = {
2197
+ attempted: false,
2198
+ ok: false,
2199
+ phase: "checkpoint",
2200
+ error: compactChatRuntimeError(error)
2201
+ };
2202
+ } else {
2203
+ checkpointInProgressCandidate({
2204
+ event: "resume_image_capture_reacquire",
2205
+ error: compactChatRuntimeError(error),
2206
+ checkpoint_id: resumeCheckpoint.checkpoint_id || null
2207
+ });
2208
+ detailStep = "resume_image_capture_reacquire";
2209
+ const resumeAttempt = await attemptImageCaptureCheckpointResume({
2210
+ checkpoint: resumeCheckpoint,
2211
+ reacquire: () => reacquireImageCaptureResumeTarget({
2212
+ domain: "chat",
2213
+ getRoots: () => getChatRoots(client),
2214
+ ensureViewport: (freshRoots) => ensureChatViewport(
2215
+ freshRoots,
2216
+ "image_capture_resume_reacquire"
2217
+ ),
2218
+ getDetailState: () => waitForChatResumeModal(client, {
2219
+ timeoutMs: 4000,
2220
+ intervalMs: 200
2221
+ }),
2222
+ isDetailAvailable: (state) => Boolean(
2223
+ !state?.forbidden_top_level_navigation
2224
+ && (state?.popup || state?.content || state?.resumeIframe)
2225
+ ),
2226
+ waitForTarget: (detailState) => waitForCvCaptureTarget(client, detailState, {
2227
+ domain: "chat",
2228
+ timeoutMs: 4000,
2229
+ intervalMs: 200
2230
+ })
2231
+ }),
2232
+ capture: (reacquired) => {
2233
+ rootState = reacquired.root_state;
2234
+ resumeState = reacquired.detail_state;
2235
+ captureTarget = reacquired.target;
2236
+ captureTargetWait = reacquired.target_wait;
2237
+ detailStep = "resume_image_capture";
2238
+ return measureTiming(
2239
+ timings,
2240
+ "screenshot_capture_ms",
2241
+ () => captureImageForTarget(captureTarget, captureTargetWait, resumeCheckpoint)
2242
+ );
2243
+ }
2244
+ });
2245
+ if (resumeAttempt.outcome === "reacquire_failed") {
2246
+ const reacquireError = resumeAttempt.error;
2247
+ imageEvidence = createRequiredImageEvidenceFailure({
2248
+ code: reacquireError?.code || "IMAGE_CAPTURE_RESUME_REACQUIRE_FAILED",
2249
+ message: reacquireError?.message || "Chat CV target reacquisition failed",
2250
+ metadata: {
2251
+ domain: "chat",
2252
+ candidate_key: candidateKey,
2253
+ resumed_from_checkpoint: true,
2254
+ resume_checkpoint_id: resumeCheckpoint.checkpoint_id || null
2255
+ }
2256
+ });
2257
+ imageEvidence.resumed_from_checkpoint = true;
2258
+ imageEvidence.resume_checkpoint_id = resumeCheckpoint.checkpoint_id || null;
2259
+ imageEvidence.coverage_checkpoint = resumeCheckpoint;
2260
+ timings.image_capture_resume = {
2261
+ attempted: true,
2262
+ ok: false,
2263
+ phase: "reacquire",
2264
+ checkpoint_id: resumeCheckpoint.checkpoint_id || null,
2265
+ error: compactChatRuntimeError(reacquireError)
2266
+ };
2267
+ }
2268
+ if (resumeAttempt.outcome === "completed") {
2269
+ imageEvidence = requireCompleteImageEvidence(resumeAttempt.evidence, {
2270
+ code: "IMAGE_CAPTURE_RESUME_EVIDENCE_MISSING",
2271
+ message: "Chat resumed CV capture returned no complete persisted evidence",
2272
+ metadata: { domain: "chat", candidate_key: candidateKey }
2273
+ });
2274
+ timings.image_capture_resume = {
2275
+ attempted: true,
2276
+ ok: !isIncompleteImageEvidence(imageEvidence),
2277
+ phase: "capture",
2278
+ checkpoint_id: resumeCheckpoint.checkpoint_id || null,
2279
+ confirmed_screenshot_count: resumeCheckpoint.unique_screenshot_count || 0
2280
+ };
2281
+ } else if (resumeAttempt.outcome === "capture_failed") {
2282
+ const resumeError = resumeAttempt.error;
2283
+ const failedCheckpoint = resumeError.capture_checkpoint || resumeCheckpoint;
2284
+ const failedScreenshots = Array.isArray(failedCheckpoint?.screenshots)
2285
+ ? failedCheckpoint.screenshots
2286
+ : [];
2287
+ imageEvidence = {
2288
+ schema_version: 1,
2289
+ ok: false,
2290
+ source: "image-scroll-sequence",
2291
+ elapsed_ms: timings.screenshot_capture_ms || 0,
2292
+ capture_count: failedCheckpoint?.confirmed_capture_count || failedScreenshots.length,
2293
+ screenshot_count: failedScreenshots.length,
2294
+ unique_screenshot_count: failedCheckpoint?.unique_screenshot_count || 0,
2295
+ coverage_complete: false,
2296
+ resumed_from_checkpoint: true,
2297
+ resume_checkpoint_id: resumeCheckpoint.checkpoint_id || null,
2298
+ error_code: resumeError?.code || "IMAGE_CAPTURE_RESUME_FAILED",
2299
+ error: resumeError?.message || String(resumeError),
2300
+ file_paths: failedScreenshots.map((item) => item?.file_path).filter(Boolean),
2301
+ llm_file_paths: [],
2302
+ coverage_checkpoint: failedCheckpoint
2303
+ };
2304
+ timings.image_capture_resume = {
2305
+ attempted: true,
2306
+ ok: false,
2307
+ phase: "capture",
2308
+ checkpoint_id: resumeCheckpoint.checkpoint_id || null,
2309
+ error: compactChatRuntimeError(resumeError)
2310
+ };
2311
+ }
2312
+ }
2313
+ }
2314
+ source = isIncompleteImageEvidence(imageEvidence) ? "image_capture_failed" : "image";
2315
+ fullCvEvidence = summarizeChatFullCvEvidence({
1719
2316
  detailResult,
1720
2317
  contentWait,
1721
2318
  imageEvidence
@@ -1728,35 +2325,47 @@ export async function runChatWorkflow({
1728
2325
  waitResult: networkWait,
1729
2326
  imageEvidence
1730
2327
  });
1731
- if (callLlmOnImage && fullCvEvidence.full_cv_acquired) {
2328
+ if (
2329
+ callLlmOnImage
2330
+ && hasScreenableChatFullCvEvidence(fullCvEvidence, imageEvidence)
2331
+ ) {
1732
2332
  detailStep = "llm_image_screening";
1733
2333
  if (!llmConfig) {
1734
2334
  llmResult = createMissingLlmConfigResult();
1735
2335
  } else {
1736
2336
  try {
1737
- llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1738
- candidate: detailResult.candidate,
1739
- criteria,
1740
- config: llmConfig,
1741
- timeoutMs: llmTimeoutMs,
2337
+ llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
2338
+ candidate: detailResult.candidate,
2339
+ criteria,
2340
+ config: llmConfig,
2341
+ timeoutMs: llmTimeoutMs,
1742
2342
  imageEvidence,
1743
2343
  maxImages: llmImageLimit,
1744
2344
  imageDetail: llmImageDetail
1745
2345
  }));
1746
- } catch (error) {
1747
- if (isFatalLlmProviderError(error)) {
1748
- throw createFatalLlmRunError(error, {
1749
- domain: "chat",
1750
- candidate: detailResult.candidate
1751
- });
1752
- }
1753
- llmResult = createFailedLlmResult(error);
1754
- }
2346
+ } catch (error) {
2347
+ if (isFatalLlmProviderError(error)) {
2348
+ throw createFatalLlmRunError(error, {
2349
+ domain: "chat",
2350
+ candidate: detailResult.candidate
2351
+ });
2352
+ }
2353
+ llmResult = createFailedLlmResult(error);
2354
+ }
1755
2355
  }
1756
2356
  }
1757
- } else {
1758
- source = "missing_capture_node";
1759
- fullCvEvidence = summarizeChatFullCvEvidence({
2357
+ } else {
2358
+ source = "missing_capture_node";
2359
+ imageEvidence = createRequiredImageEvidenceFailure({
2360
+ code: "IMAGE_CAPTURE_TARGET_UNAVAILABLE",
2361
+ message: "Chat CV capture target was unavailable after the network fallback missed",
2362
+ metadata: {
2363
+ domain: "chat",
2364
+ candidate_key: candidateKey,
2365
+ capture_target_wait: captureTargetWait || null
2366
+ }
2367
+ });
2368
+ fullCvEvidence = summarizeChatFullCvEvidence({
1760
2369
  detailResult,
1761
2370
  contentWait,
1762
2371
  imageEvidence
@@ -1801,10 +2410,16 @@ export async function runChatWorkflow({
1801
2410
  });
1802
2411
  }
1803
2412
 
1804
- if (useLlmScreening && !llmResult) {
1805
- if (!fullCvEvidence.full_cv_acquired) {
1806
- detailUnavailableReason = "full_cv_not_acquired";
1807
- } else {
2413
+ if (useLlmScreening && !llmResult) {
2414
+ if (!hasScreenableChatFullCvEvidence(fullCvEvidence, imageEvidence)) {
2415
+ if (isIncompleteImageEvidence(imageEvidence)) {
2416
+ detailUnavailableReason = `image_capture_failed:${String(
2417
+ imageEvidence?.error_code || "coverage_incomplete"
2418
+ ).toLowerCase()}`;
2419
+ } else {
2420
+ detailUnavailableReason = "full_cv_not_acquired";
2421
+ }
2422
+ } else {
1808
2423
  detailStep = "llm_screening";
1809
2424
  if (!llmConfig) {
1810
2425
  llmResult = createMissingLlmConfigResult();
@@ -1813,24 +2428,24 @@ export async function runChatWorkflow({
1813
2428
  const llmTimingKey = imageEvidence?.file_paths?.length
1814
2429
  ? "vision_model_ms"
1815
2430
  : "text_model_ms";
1816
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1817
- candidate: detailResult.candidate,
1818
- criteria,
1819
- config: llmConfig,
2431
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
2432
+ candidate: detailResult.candidate,
2433
+ criteria,
2434
+ config: llmConfig,
1820
2435
  timeoutMs: llmTimeoutMs,
1821
2436
  imageEvidence,
1822
2437
  maxImages: llmImageLimit,
1823
2438
  imageDetail: llmImageDetail
1824
2439
  }));
1825
- } catch (error) {
1826
- if (isFatalLlmProviderError(error)) {
1827
- throw createFatalLlmRunError(error, {
1828
- domain: "chat",
1829
- candidate: detailResult.candidate
1830
- });
1831
- }
1832
- llmResult = createFailedLlmResult(error);
1833
- }
2440
+ } catch (error) {
2441
+ if (isFatalLlmProviderError(error)) {
2442
+ throw createFatalLlmRunError(error, {
2443
+ domain: "chat",
2444
+ candidate: detailResult.candidate
2445
+ });
2446
+ }
2447
+ llmResult = createFailedLlmResult(error);
2448
+ }
1834
2449
  }
1835
2450
  }
1836
2451
  }
@@ -1891,24 +2506,36 @@ export async function runChatWorkflow({
1891
2506
  event: "detail_error",
1892
2507
  error: compactChatRuntimeError(error)
1893
2508
  });
1894
- if (isForbiddenChatResumeNavigationError(error)) {
2509
+ if (isForbiddenChatResumeNavigationError(error)) {
1895
2510
  detailUnavailableReason = "forbidden_top_level_resume_navigation";
1896
2511
  const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
1897
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1898
- detailResult.cv_acquisition.recovery = recovery;
1899
- } else if (isChatResumeModalCloseFailureError(error)) {
1900
- const recoveryReason = `resume_modal_close_failed:${detailStep}`;
1901
- const recovery = await recoverAndReapplyChatContext(recoveryReason, error, { forceRefresh: true });
1902
- checkpointInProgressCandidate({
1903
- event: "retry_after_modal_recovery",
1904
- recovery
1905
- });
1906
- continue;
1907
- } else if (isChatCandidateSelectionMismatchError(error)) {
1908
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1909
- if (retryCount < 1) {
1910
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1911
- const recovery = await recoverAndReapplyChatContext(
2512
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
2513
+ detailResult.cv_acquisition.recovery = recovery;
2514
+ } else if (isChatResumeModalCloseFailureError(error)) {
2515
+ const retryReservation = consumeChatDetailRecoveryBudget(
2516
+ candidateRecoveryCounts,
2517
+ candidateKey
2518
+ );
2519
+ if (retryReservation.allowed) {
2520
+ const recoveryReason = `resume_modal_close_failed:${detailStep}`;
2521
+ const recovery = await recoverAndReapplyChatContext(recoveryReason, error, { forceRefresh: true });
2522
+ checkpointInProgressCandidate({
2523
+ event: "retry_after_modal_recovery",
2524
+ recovery,
2525
+ retry: retryReservation
2526
+ });
2527
+ continue;
2528
+ }
2529
+ detailUnavailableReason = "resume_modal_close_failed";
2530
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
2531
+ detailResult.cv_acquisition.recovery_attempted = true;
2532
+ detailResult.cv_acquisition.recovery_attempt_count = retryReservation.count;
2533
+ detailResult.cv_acquisition.recovery_retry_limit = retryReservation.retry_limit;
2534
+ } else if (isChatCandidateSelectionMismatchError(error)) {
2535
+ const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
2536
+ if (retryCount < 1) {
2537
+ candidateRecoveryCounts.set(candidateKey, retryCount + 1);
2538
+ const recovery = await recoverAndReapplyChatContext(
1912
2539
  "active_candidate_mismatch",
1913
2540
  error,
1914
2541
  { forceRefresh: true }
@@ -1916,42 +2543,55 @@ export async function runChatWorkflow({
1916
2543
  checkpointInProgressCandidate({
1917
2544
  event: "retry_after_active_candidate_mismatch_recovery",
1918
2545
  recovery
1919
- });
1920
- continue;
1921
- }
1922
- detailUnavailableReason = "active_candidate_mismatch";
1923
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1924
- detailResult.cv_acquisition.selection_ready_state = error.selection_ready_state || null;
1925
- detailResult.cv_acquisition.recovery_attempted = true;
1926
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1927
- } else if (isChatOnlineResumeModalOpenFailureError(error)) {
1928
- const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1929
- if (retryCount < 1) {
1930
- candidateRecoveryCounts.set(candidateKey, retryCount + 1);
1931
- const recovery = await recoverAndReapplyChatContext(
1932
- "online_resume_modal_did_not_open",
1933
- error,
1934
- { forceRefresh: true }
1935
- );
1936
- checkpointInProgressCandidate({
1937
- event: "retry_after_online_resume_modal_open_failure",
1938
- recovery
1939
- });
1940
- continue;
1941
- }
1942
- detailUnavailableReason = "online_resume_modal_did_not_open";
1943
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1944
- detailResult.cv_acquisition.attempts = error.attempts || null;
1945
- detailResult.cv_acquisition.recovery_attempted = true;
1946
- detailResult.cv_acquisition.recovery_attempt_count = retryCount;
1947
- } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1948
- detailUnavailableReason = "unsafe_online_resume_navigation_link";
1949
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1950
- detailResult.cv_acquisition.blocked_pre_click = true;
1951
- detailResult.cv_acquisition.button_href = error.href || null;
2546
+ });
2547
+ continue;
2548
+ }
2549
+ error.retryable = false;
2550
+ checkpointInProgressCandidate({
2551
+ event: "active_candidate_mismatch_exhausted",
2552
+ selection_ready_state: compactChatCandidateSelectionReadyState(error.selection_ready_state),
2553
+ recovery_attempted: true,
2554
+ recovery_attempt_count: retryCount
2555
+ });
2556
+ throw error;
2557
+ } else if (isChatOnlineResumeModalOpenFailureError(error)) {
2558
+ const retryCount = candidateRecoveryCounts.get(candidateKey) || 0;
2559
+ if (retryCount < 1) {
2560
+ candidateRecoveryCounts.set(candidateKey, retryCount + 1);
2561
+ const recovery = await recoverAndReapplyChatContext(
2562
+ "online_resume_modal_did_not_open",
2563
+ error,
2564
+ { forceRefresh: true }
2565
+ );
2566
+ checkpointInProgressCandidate({
2567
+ event: "retry_after_online_resume_modal_open_failure",
2568
+ recovery
2569
+ });
2570
+ continue;
2571
+ }
2572
+ detailUnavailableReason = "online_resume_modal_did_not_open";
2573
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
2574
+ detailResult.cv_acquisition.attempts = error.attempts || null;
2575
+ detailResult.cv_acquisition.recovery_attempted = true;
2576
+ detailResult.cv_acquisition.recovery_attempt_count = retryCount;
2577
+ } else if (isUnsafeChatOnlineResumeLinkError(error)) {
2578
+ detailUnavailableReason = "unsafe_online_resume_navigation_link";
2579
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
2580
+ detailResult.cv_acquisition.blocked_pre_click = true;
2581
+ detailResult.cv_acquisition.button_href = error.href || null;
1952
2582
  detailResult.cv_acquisition.button_selector = error.button_selector || null;
1953
2583
  detailResult.cv_acquisition.attempts = error.attempts || null;
1954
- } else {
2584
+ } else if (isRecoverableChatImageCaptureError(error)) {
2585
+ detailUnavailableReason = `image_capture_failed:${String(error?.code || "unknown").toLowerCase()}`;
2586
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
2587
+ detailResult.cv_acquisition.recovery_attempted = true;
2588
+ detailResult.cv_acquisition.recovery_attempt_count = imageCaptureWorkflowRetries.count(candidateKey);
2589
+ detailResult.image_evidence = createRequiredImageEvidenceFailure({
2590
+ code: error?.code || "IMAGE_CAPTURE_FAILED",
2591
+ message: error?.message || "Chat CV image capture failed",
2592
+ metadata: { domain: "chat", candidate_key: candidateKey }
2593
+ });
2594
+ } else {
1955
2595
  if (!isRecoverableCdpNodeError(error)) throw error;
1956
2596
  detailUnavailableReason = `recoverable_cdp_node_stale:${detailStep}`;
1957
2597
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
@@ -1959,62 +2599,437 @@ export async function runChatWorkflow({
1959
2599
  await closeChatBlockingPanels(client, { attemptsLimit: 2 });
1960
2600
  }
1961
2601
  }
1962
- screeningCandidate = detailResult?.candidate || cardCandidate;
1963
- }
2602
+ screeningCandidate = detailResult?.candidate || cardCandidate;
2603
+ }
1964
2604
 
1965
2605
  await runControl.waitIfPaused();
1966
2606
  runControl.throwIfCanceled();
1967
2607
  runControl.setPhase("chat:screening");
1968
2608
  let cardOnlyLlmResult = null;
1969
- if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1970
- detailUnavailableReason = detailResult
1971
- ? "full_cv_not_acquired"
1972
- : "detail_not_opened_full_cv_required";
1973
- }
1974
- const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1975
- const screening = collectCvOnly
1976
- ? createCvCollectionScreening(screeningCandidate, {
1977
- detailResult,
1978
- detailUnavailableReason,
1979
- preActionState
1980
- })
1981
- : detailUnavailableReason
1982
- ? {
1983
- status: "skip",
1984
- passed: false,
1985
- score: 0,
1986
- reasons: [detailUnavailableReason],
1987
- candidate: screeningCandidate
1988
- }
1989
- : useLlmScreening
1990
- ? llmToScreening(effectiveLlmResult, screeningCandidate)
1991
- : screenCandidate(screeningCandidate, { criteria });
1992
- let postAction = null;
1993
- if (requestResumeForPassed && screening.passed) {
1994
- await maybeHumanActionCooldown("before_post_action", timings);
1995
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1996
- greetingText,
1997
- dryRun: dryRunRequestCv
1998
- }));
1999
- if (postAction?.requested) requestSatisfiedCount += 1;
2000
- if (postAction?.skipped) requestSkippedCount += 1;
2001
- if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
2002
- if (!postAction?.requested && !postAction?.skipped && !dryRunRequestCv) {
2003
- throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
2004
- }
2005
- }
2006
- timings.total_ms = Date.now() - candidateStarted;
2007
- const compactResult = {
2609
+ if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
2610
+ detailUnavailableReason = detailResult
2611
+ ? "full_cv_not_acquired"
2612
+ : "detail_not_opened_full_cv_required";
2613
+ }
2614
+ const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
2615
+ let screening = collectCvOnly
2616
+ ? createCvCollectionScreening(screeningCandidate, {
2617
+ detailResult,
2618
+ detailUnavailableReason,
2619
+ preActionState
2620
+ })
2621
+ : detailUnavailableReason
2622
+ ? {
2623
+ status: "skip",
2624
+ passed: false,
2625
+ score: 0,
2626
+ reasons: [detailUnavailableReason],
2627
+ candidate: screeningCandidate
2628
+ }
2629
+ : useLlmScreening
2630
+ ? llmToScreening(effectiveLlmResult, screeningCandidate)
2631
+ : screenCandidate(screeningCandidate, { criteria });
2632
+ let postAction = preScreeningActionReconciliation;
2633
+ if (requestResumeForPassed && screening.passed) {
2634
+ await maybeHumanActionCooldown("before_post_action", timings);
2635
+ postAction = await measureTiming(timings, "post_action_ms", async () => {
2636
+ if (dryRunRequestCv) {
2637
+ return requestChatResumeForPassedCandidate(client, {
2638
+ greetingText,
2639
+ dryRun: true
2640
+ });
2641
+ }
2642
+
2643
+ const candidateId = normalizeText(screeningCandidate.id || cardCandidate.id || "");
2644
+ if (!candidateId) {
2645
+ const error = new Error("CHAT_ACTION_CANDIDATE_ID_REQUIRED: refusing outbound action without a stable Boss candidate ID");
2646
+ error.code = "CHAT_ACTION_CANDIDATE_ID_REQUIRED";
2647
+ error.retryable = false;
2648
+ throw error;
2649
+ }
2650
+ if (!effectiveActionJournal) {
2651
+ const error = new Error("CHAT_ACTION_JOURNAL_REQUIRED: durable outbound action journal is unavailable");
2652
+ error.code = "CHAT_ACTION_JOURNAL_REQUIRED";
2653
+ error.retryable = false;
2654
+ throw error;
2655
+ }
2656
+ if (typeof runControl.checkpointCritical !== "function") {
2657
+ const error = new Error("CHAT_CRITICAL_CHECKPOINT_UNAVAILABLE: refusing outbound action without required checkpoint persistence");
2658
+ error.code = "CHAT_CRITICAL_CHECKPOINT_UNAVAILABLE";
2659
+ error.retryable = false;
2660
+ throw error;
2661
+ }
2662
+
2663
+ const activeState = await readChatActiveCandidateState(client);
2664
+ const activeCandidateId = normalizeText(activeState?.active_candidate?.candidate_id || "");
2665
+ if (!activeCandidateId || activeCandidateId !== candidateId) {
2666
+ const error = new Error(`CHAT_ACTIVE_CANDIDATE_MISMATCH: expected=${candidateId}; active=${activeCandidateId || "unknown"}`);
2667
+ error.code = "CHAT_ACTIVE_CANDIDATE_MISMATCH";
2668
+ error.retryable = false;
2669
+ throw error;
2670
+ }
2671
+
2672
+ let journalRecord = effectiveActionJournal.read({
2673
+ scope: actionJournalScope,
2674
+ candidateId
2675
+ });
2676
+ const transitionAction = async (state, evidence = {}) => {
2677
+ const transitioned = effectiveActionJournal.transition({
2678
+ scope: actionJournalScope,
2679
+ candidateId,
2680
+ state,
2681
+ runId: runControl.runId,
2682
+ greeting: greetingText,
2683
+ evidence: {
2684
+ ...evidence,
2685
+ active_candidate_id: activeCandidateId
2686
+ }
2687
+ });
2688
+ journalRecord = transitioned.record;
2689
+ runControl.checkpointCritical({
2690
+ action_transaction: compactChatActionJournalRecord(
2691
+ transitioned.record,
2692
+ transitioned.file_path
2693
+ )
2694
+ });
2695
+ return transitioned.record;
2696
+ };
2697
+
2698
+ const currentReadyState = await readChatConversationReadyState(client);
2699
+ if (!journalRecord && (
2700
+ currentReadyState.attachment_resume_enabled
2701
+ || currentReadyState.already_requested_resume
2702
+ )) {
2703
+ return {
2704
+ requested: false,
2705
+ skipped: true,
2706
+ reason: currentReadyState.attachment_resume_enabled
2707
+ ? "attachment_resume_already_available"
2708
+ : "resume_request_already_pending",
2709
+ initial_state: currentReadyState,
2710
+ journal_bypassed_existing_state: true
2711
+ };
2712
+ }
2713
+
2714
+ if (!journalRecord) {
2715
+ journalRecord = await transitionAction("pre_action", {
2716
+ active_candidate_id: activeCandidateId
2717
+ });
2718
+ const existingGreeting = await getChatGreetingMessageState(client, greetingText);
2719
+ if (existingGreeting.ok && existingGreeting.exact_count > 0) {
2720
+ journalRecord = await transitionAction("greeting_send_in_flight", {
2721
+ greeting_baseline_count: Math.max(0, existingGreeting.exact_count - 1),
2722
+ greeting_evidence_readable: true,
2723
+ reason: "preexisting_exact_greeting_detected"
2724
+ });
2725
+ journalRecord = await transitionAction("greeting_confirmed", {
2726
+ greeting_baseline_count: Math.max(0, existingGreeting.exact_count - 1),
2727
+ greeting_evidence_readable: true,
2728
+ reason: "preexisting_exact_greeting_detected"
2729
+ });
2730
+ }
2731
+ }
2732
+
2733
+ if (journalRecord.state === "request_confirmed") {
2734
+ return {
2735
+ requested: false,
2736
+ skipped: true,
2737
+ satisfied: true,
2738
+ reason: "request_confirmed_by_durable_journal",
2739
+ action_transaction: compactChatActionJournalRecord(journalRecord)
2740
+ };
2741
+ }
2742
+
2743
+ if (
2744
+ journalRecord.state === "greeting_confirmed"
2745
+ && (currentReadyState.attachment_resume_enabled || currentReadyState.already_requested_resume)
2746
+ ) {
2747
+ await transitionAction("request_in_flight", {
2748
+ request_baseline_count: 0,
2749
+ resume_attachment_baseline_count: 0,
2750
+ reason: "existing_request_state_before_input"
2751
+ });
2752
+ journalRecord = await transitionAction("request_confirmed", {
2753
+ reason: currentReadyState.attachment_resume_enabled
2754
+ ? "attachment_resume_already_available"
2755
+ : "resume_request_already_pending"
2756
+ });
2757
+ return {
2758
+ requested: false,
2759
+ skipped: true,
2760
+ satisfied: true,
2761
+ reason: "request_confirmed_from_existing_state",
2762
+ action_transaction: compactChatActionJournalRecord(journalRecord)
2763
+ };
2764
+ }
2765
+
2766
+ const unknownOrigin = chatActionUnknownOrigin(journalRecord);
2767
+ if (
2768
+ journalRecord.state === "request_in_flight"
2769
+ || (journalRecord.state === "outcome_unknown" && unknownOrigin === "request_in_flight")
2770
+ ) {
2771
+ const {
2772
+ ready_state: readyEvidence,
2773
+ message_state: messageEvidence
2774
+ } = await readChatRequestVerificationEvidence(client);
2775
+ const baselineCount = Number(journalRecord.evidence?.request_baseline_count) || 0;
2776
+ const baselineAttachmentCount = Number(journalRecord.evidence?.resume_attachment_baseline_count) || 0;
2777
+ const requestConfirmed = Boolean(
2778
+ readyEvidence.attachment_resume_enabled
2779
+ || readyEvidence.already_requested_resume
2780
+ || messageEvidence.count > baselineCount
2781
+ || messageEvidence.resume_attachment_count > baselineAttachmentCount
2782
+ );
2783
+ if (requestConfirmed) {
2784
+ journalRecord = await transitionAction("request_confirmed", {
2785
+ reason: "recovered_from_exact_request_evidence",
2786
+ message_observed: messageEvidence.count > baselineCount,
2787
+ request_after_count: messageEvidence.count,
2788
+ resume_attachment_after_count: messageEvidence.resume_attachment_count
2789
+ });
2790
+ return {
2791
+ requested: false,
2792
+ skipped: true,
2793
+ satisfied: true,
2794
+ reason: "request_confirmed_after_in_flight_recovery",
2795
+ action_transaction: compactChatActionJournalRecord(journalRecord)
2796
+ };
2797
+ }
2798
+ const preservedUnknown = preserveChatRequestOutcomeUnknownWithoutReplay({
2799
+ actionJournalRecord: journalRecord,
2800
+ candidateId,
2801
+ activeCandidateId,
2802
+ readyEvidence,
2803
+ messageEvidence
2804
+ });
2805
+ if (preservedUnknown) return preservedUnknown;
2806
+ if (journalRecord.state !== "outcome_unknown") {
2807
+ journalRecord = await transitionAction("outcome_unknown", {
2808
+ action: "request_resume",
2809
+ reason: "in_flight_request_not_provable"
2810
+ });
2811
+ }
2812
+ throw makeChatPostActionOutcomeUnknownError(candidateId, journalRecord, {
2813
+ ready_state_readable: Boolean(readyEvidence),
2814
+ message_state_readable: Boolean(messageEvidence.ok)
2815
+ });
2816
+ }
2817
+
2818
+ let skipGreeting = journalRecord.state === "greeting_confirmed";
2819
+ if (
2820
+ journalRecord.state === "greeting_send_in_flight"
2821
+ || (journalRecord.state === "outcome_unknown" && unknownOrigin === "greeting_send_in_flight")
2822
+ ) {
2823
+ const greetingEvidence = await getChatGreetingMessageState(client, greetingText);
2824
+ const baselineCount = Number(journalRecord.evidence?.greeting_baseline_count) || 0;
2825
+ if (greetingEvidence.ok && greetingEvidence.exact_count > baselineCount) {
2826
+ journalRecord = await transitionAction("greeting_confirmed", {
2827
+ reason: "recovered_from_exact_greeting_delta",
2828
+ greeting_baseline_count: baselineCount,
2829
+ greeting_evidence_readable: true
2830
+ });
2831
+ skipGreeting = true;
2832
+ } else {
2833
+ if (journalRecord.state !== "outcome_unknown") {
2834
+ journalRecord = await transitionAction("outcome_unknown", {
2835
+ action: "greeting_send",
2836
+ reason: greetingEvidence.ok
2837
+ ? "in_flight_greeting_delta_absent"
2838
+ : "in_flight_greeting_evidence_unreadable",
2839
+ greeting_baseline_count: baselineCount,
2840
+ greeting_evidence_readable: greetingEvidence.ok
2841
+ });
2842
+ }
2843
+ throw makeChatPostActionOutcomeUnknownError(candidateId, journalRecord, {
2844
+ greeting_evidence_readable: greetingEvidence.ok,
2845
+ greeting_exact_count: greetingEvidence.exact_count,
2846
+ greeting_baseline_count: baselineCount
2847
+ });
2848
+ }
2849
+ }
2850
+
2851
+ if (journalRecord.state === "outcome_unknown") {
2852
+ throw makeChatPostActionOutcomeUnknownError(candidateId, journalRecord);
2853
+ }
2854
+ let editorPreActionRetryCount = 0;
2855
+ while (true) {
2856
+ try {
2857
+ const result = await requestChatResumeForPassedCandidate(client, {
2858
+ greetingText,
2859
+ dryRun: false,
2860
+ skipGreeting,
2861
+ actionTransition: transitionAction
2862
+ });
2863
+ if (editorPreActionRetryCount > 0) {
2864
+ return {
2865
+ ...result,
2866
+ editor_pre_action_recovery: {
2867
+ recovered: true,
2868
+ retry_count: editorPreActionRetryCount,
2869
+ settle_ms: CHAT_EDITOR_PRE_ACTION_RETRY_SETTLE_MS
2870
+ }
2871
+ };
2872
+ }
2873
+ return result;
2874
+ } catch (error) {
2875
+ if (!shouldRetryChatEditorPreAction(
2876
+ error,
2877
+ journalRecord,
2878
+ editorPreActionRetryCount
2879
+ )) {
2880
+ throw error;
2881
+ }
2882
+
2883
+ editorPreActionRetryCount += 1;
2884
+ runControl.checkpoint({
2885
+ chat_editor_pre_action_recovery: {
2886
+ candidate_id: candidateId,
2887
+ retry_count: editorPreActionRetryCount,
2888
+ settle_ms: CHAT_EDITOR_PRE_ACTION_RETRY_SETTLE_MS,
2889
+ reason: error?.code || error?.message || "CHAT_EDITOR_MESSAGE_MISMATCH",
2890
+ action_state: journalRecord.state,
2891
+ attempts: Array.isArray(error?.attempts) ? error.attempts : []
2892
+ }
2893
+ });
2894
+ await runControl.sleep(CHAT_EDITOR_PRE_ACTION_RETRY_SETTLE_MS);
2895
+ runControl.throwIfCanceled();
2896
+
2897
+ const topLevelState = await getChatTopLevelState(client);
2898
+ if (topLevelState.is_security_verification) {
2899
+ throw makeBossSecurityVerificationRequiredError(
2900
+ topLevelState,
2901
+ "chat_editor_pre_action_recovery"
2902
+ );
2903
+ }
2904
+ const latestJournalRecord = effectiveActionJournal.read({
2905
+ scope: actionJournalScope,
2906
+ candidateId
2907
+ });
2908
+ if (latestJournalRecord?.state !== "pre_action") {
2909
+ throw error;
2910
+ }
2911
+ journalRecord = latestJournalRecord;
2912
+
2913
+ const retryActiveState = await readChatActiveCandidateState(client);
2914
+ const retryActiveCandidateId = normalizeText(
2915
+ retryActiveState?.active_candidate?.candidate_id || ""
2916
+ );
2917
+ if (!retryActiveCandidateId || retryActiveCandidateId !== candidateId) {
2918
+ const mismatchError = new Error(
2919
+ `CHAT_ACTIVE_CANDIDATE_MISMATCH: expected=${candidateId}; active=${retryActiveCandidateId || "unknown"}`
2920
+ );
2921
+ mismatchError.code = "CHAT_ACTIVE_CANDIDATE_MISMATCH";
2922
+ mismatchError.retryable = false;
2923
+ throw mismatchError;
2924
+ }
2925
+
2926
+ const retryReadyState = await readChatConversationReadyState(client);
2927
+ if (
2928
+ retryReadyState.attachment_resume_enabled
2929
+ || retryReadyState.already_requested_resume
2930
+ ) {
2931
+ return {
2932
+ requested: false,
2933
+ skipped: true,
2934
+ reason: retryReadyState.attachment_resume_enabled
2935
+ ? "attachment_resume_already_available"
2936
+ : "resume_request_already_pending",
2937
+ initial_state: retryReadyState,
2938
+ editor_pre_action_recovery: {
2939
+ recovered: true,
2940
+ retry_count: editorPreActionRetryCount,
2941
+ settle_ms: CHAT_EDITOR_PRE_ACTION_RETRY_SETTLE_MS,
2942
+ converged_to_existing_state: true
2943
+ }
2944
+ };
2945
+ }
2946
+ }
2947
+ }
2948
+ });
2949
+ screening = applyChatProtectedOutcomeSkip(screening, postAction, screeningCandidate);
2950
+ if (postAction?.requested) requestSatisfiedCount += 1;
2951
+ if (postAction?.satisfied && !postAction?.requested) requestSatisfiedCount += 1;
2952
+ if (postAction?.skipped) requestSkippedCount += 1;
2953
+ if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
2954
+ if (!postAction?.requested && !postAction?.skipped && !dryRunRequestCv) {
2955
+ const verificationError = new Error(
2956
+ `REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`
2957
+ );
2958
+ verificationError.code = "REQUEST_CV_NOT_VERIFIED";
2959
+ verificationError.retryable = false;
2960
+ verificationError.attempts = Array.isArray(postAction?.attempts)
2961
+ ? postAction.attempts
2962
+ : [];
2963
+ throw verificationError;
2964
+ }
2965
+ }
2966
+ // Every committed collect-CV outcome converges here: existing attachment/online CV,
2967
+ // an already-pending request, or a newly requested CV. Human-rest and delayMs remain later layers.
2968
+ let processingFloor = null;
2969
+ if (collectCvProcessingFloorEnabled) {
2970
+ const elapsedBeforeFloor = Math.max(0, Date.now() - candidateProcessingStarted);
2971
+ const floorCurrent = {
2972
+ candidate_id: screeningCandidate.id || null,
2973
+ candidate_key: candidateKey,
2974
+ target_ms: candidateProcessingFloorTargetMs,
2975
+ elapsed_before_ms: elapsedBeforeFloor,
2976
+ remaining_ms: Math.max(0, candidateProcessingFloorTargetMs - elapsedBeforeFloor)
2977
+ };
2978
+ runControl.setPhase("chat:collect_cv_processing_floor");
2979
+ runControl.updateProgress({
2980
+ collect_cv_processing_floor_current: floorCurrent
2981
+ });
2982
+ runControl.checkpoint({
2983
+ collect_cv_processing_floor_current: floorCurrent
2984
+ });
2985
+ processingFloor = await enforceChatCollectCvProcessingFloor({
2986
+ candidateStartedAt: candidateProcessingStarted,
2987
+ targetMs: candidateProcessingFloorTargetMs,
2988
+ sleepFn: (ms) => runControl.sleep(ms)
2989
+ });
2990
+ collectCvProcessingFloorState.count += 1;
2991
+ collectCvProcessingFloorState.total_delay_requested_ms += processingFloor.delay_requested_ms;
2992
+ collectCvProcessingFloorState.total_delay_elapsed_ms += processingFloor.delay_elapsed_ms;
2993
+ collectCvProcessingFloorState.last = {
2994
+ candidate_id: screeningCandidate.id || null,
2995
+ candidate_key: candidateKey,
2996
+ ...processingFloor
2997
+ };
2998
+ timings.collect_cv_processing_target_ms = processingFloor.target_ms;
2999
+ timings.collect_cv_processing_elapsed_before_floor_ms = processingFloor.elapsed_before_ms;
3000
+ timings.collect_cv_processing_floor_delay_requested_ms = processingFloor.delay_requested_ms;
3001
+ timings.collect_cv_processing_floor_delay_ms = processingFloor.delay_elapsed_ms;
3002
+ timings.collect_cv_processing_elapsed_after_floor_ms = processingFloor.elapsed_after_ms;
3003
+ runControl.updateProgress({
3004
+ collect_cv_processing_floor_count: collectCvProcessingFloorState.count,
3005
+ collect_cv_processing_floor_delay_requested_ms: collectCvProcessingFloorState.total_delay_requested_ms,
3006
+ collect_cv_processing_floor_delay_ms: collectCvProcessingFloorState.total_delay_elapsed_ms,
3007
+ collect_cv_processing_floor_last: collectCvProcessingFloorState.last,
3008
+ collect_cv_processing_floor_current: null
3009
+ });
3010
+ runControl.checkpoint({
3011
+ collect_cv_processing_floor_current: null,
3012
+ collect_cv_processing_floor_last: collectCvProcessingFloorState.last,
3013
+ collect_cv_processing_floor: collectCvProcessingFloorSnapshot()
3014
+ });
3015
+ }
3016
+ timings.total_ms = Date.now() - candidateStarted;
3017
+ if (index < detailCountLimit && preActionState) {
3018
+ requireExactChatCandidateSelection({ ready: selectionReadyState }, cardCandidate);
3019
+ }
3020
+ const compactResult = {
2008
3021
  index,
2009
3022
  candidate_key: candidateKey,
2010
- card_node_id: effectiveCardNodeId,
2011
- candidate: compactCandidate(screeningCandidate),
2012
- detail: compactDetail(detailResult),
3023
+ card_node_id: effectiveCardNodeId,
3024
+ candidate: compactCandidate(screeningCandidate),
3025
+ selection_ready_state: compactChatCandidateSelectionReadyState(selectionReadyState),
3026
+ detail: compactDetail(detailResult),
2013
3027
  llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
2014
- screening: compactScreening(screening),
2015
- post_action: postAction,
2016
- pre_action_state: preActionState,
2017
- timings
3028
+ screening: compactScreening(screening),
3029
+ post_action: postAction,
3030
+ pre_action_state: preActionState,
3031
+ collect_cv_processing_floor: processingFloor,
3032
+ timings
2018
3033
  };
2019
3034
  results.push(compactResult);
2020
3035
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -2046,14 +3061,22 @@ export async function runChatWorkflow({
2046
3061
  viewport_checks: viewportGuard.getStats().checks,
2047
3062
  viewport_recoveries: viewportGuard.getStats().recoveries,
2048
3063
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2049
- human_behavior_profile: effectiveHumanBehavior.profile,
2050
- human_rest_level: effectiveHumanBehavior.restLevel,
2051
- human_rest_enabled: effectiveHumanRestEnabled,
2052
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2053
- human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2054
- human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2055
- human_rest_count: humanRestController.getState().rest_count,
2056
- human_rest_ms: humanRestController.getState().total_rest_ms,
3064
+ human_behavior_profile: effectiveHumanBehavior.profile,
3065
+ human_rest_level: effectiveHumanBehavior.restLevel,
3066
+ human_rest_enabled: effectiveHumanRestEnabled,
3067
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
3068
+ human_rest_per_candidate_min_ms: null,
3069
+ human_rest_per_candidate_max_ms: null,
3070
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
3071
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorState.min_ms,
3072
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorState.max_ms,
3073
+ collect_cv_processing_floor_count: collectCvProcessingFloorState.count,
3074
+ collect_cv_processing_floor_delay_requested_ms: collectCvProcessingFloorState.total_delay_requested_ms,
3075
+ collect_cv_processing_floor_delay_ms: collectCvProcessingFloorState.total_delay_elapsed_ms,
3076
+ collect_cv_processing_floor_last: collectCvProcessingFloorState.last,
3077
+ collect_cv_processing_floor_current: null,
3078
+ human_rest_count: humanRestController.getState().rest_count,
3079
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2057
3080
  last_human_event: lastHumanEvent,
2058
3081
  last_candidate_id: screeningCandidate.id || null,
2059
3082
  last_candidate_key: candidateKey,
@@ -2093,11 +3116,11 @@ export async function runChatWorkflow({
2093
3116
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
2094
3117
  compactResult.timings.total_ms = Date.now() - candidateStarted;
2095
3118
  runControl.updateProgress({
2096
- human_rest_enabled: effectiveHumanRestEnabled,
2097
- human_rest_level: effectiveHumanBehavior.restLevel,
2098
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2099
- human_rest_count: humanRestController.getState().rest_count,
2100
- human_rest_ms: humanRestController.getState().total_rest_ms,
3119
+ human_rest_enabled: effectiveHumanRestEnabled,
3120
+ human_rest_level: effectiveHumanBehavior.restLevel,
3121
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
3122
+ human_rest_count: humanRestController.getState().rest_count,
3123
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2101
3124
  human_rest_last: restResult,
2102
3125
  context_recoveries: contextRecoveryAttempts,
2103
3126
  last_human_event: lastHumanEvent
@@ -2125,15 +3148,16 @@ export async function runChatWorkflow({
2125
3148
  stats: viewportGuard.getStats(),
2126
3149
  events: viewportGuard.getEvents()
2127
3150
  },
2128
- human_behavior: effectiveHumanBehavior,
2129
- human_rest: humanRestController.getState(),
2130
- last_human_event: lastHumanEvent,
3151
+ human_behavior: effectiveHumanBehavior,
3152
+ human_rest: humanRestController.getState(),
3153
+ collect_cv_processing_floor: collectCvProcessingFloorSnapshot(),
3154
+ last_human_event: lastHumanEvent,
2131
3155
  list_end_reason: listEndReason || null,
2132
3156
  target_pass_count: passTarget,
2133
- process_until_list_end: Boolean(processUntilListEnd),
2134
- processed_limit: processedLimit,
2135
- detail_source: normalizedDetailSource,
2136
- processed: finalCounters.processed,
3157
+ process_until_list_end: Boolean(processUntilListEnd),
3158
+ processed_limit: processedLimit,
3159
+ detail_source: normalizedDetailSource,
3160
+ processed: finalCounters.processed,
2137
3161
  screened: finalCounters.screened,
2138
3162
  detail_opened: finalCounters.detail_opened,
2139
3163
  llm_screened: finalCounters.llm_screened,
@@ -2147,13 +3171,15 @@ export async function runChatWorkflow({
2147
3171
  };
2148
3172
  }
2149
3173
 
2150
- export function createChatRunService({
2151
- lifecycle,
2152
- idPrefix = "chat",
2153
- workflow = runChatWorkflow,
2154
- onSnapshot = null
2155
- } = {}) {
2156
- const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
3174
+ export function createChatRunService({
3175
+ lifecycle,
3176
+ idPrefix = "chat",
3177
+ workflow = runChatWorkflow,
3178
+ onSnapshot = null,
3179
+ actionJournal = null
3180
+ } = {}) {
3181
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
3182
+ const configuredActionJournal = actionJournal || null;
2157
3183
 
2158
3184
  function startChatRun({
2159
3185
  runId = "",
@@ -2191,21 +3217,23 @@ export function createChatRunService({
2191
3217
  listSettleMs = 2200,
2192
3218
  listFallbackPoint = null,
2193
3219
  imageOutputDir = "",
2194
- humanRestEnabled = false,
2195
- humanBehavior = null,
2196
- name = "chat-domain-run"
3220
+ humanRestEnabled = false,
3221
+ humanBehavior = null,
3222
+ actionJournalScope = "boss-chat:default",
3223
+ name = "chat-domain-run"
2197
3224
  } = {}) {
2198
3225
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
2199
- const normalizedDetailSource = normalizeDetailSource(detailSource);
2200
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2201
- const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
2202
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2203
- const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2204
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2205
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2206
- });
2207
- const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
2208
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
3226
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
3227
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
3228
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
3229
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
3230
+ const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
3231
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
3232
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
3233
+ });
3234
+ const collectCvProcessingFloorEnabled = collectCvOnly;
3235
+ const collectCvPerCandidateRestEnabled = false;
3236
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
2209
3237
  return manager.startRun({
2210
3238
  runId,
2211
3239
  name,
@@ -2221,20 +3249,21 @@ export function createChatRunService({
2221
3249
  detail_limit: normalizedDetailLimit,
2222
3250
  detail_source: normalizedDetailSource,
2223
3251
  close_resume: closeResume,
2224
- request_resume_for_passed: Boolean(requestResumeForPassed),
3252
+ request_resume_for_passed: Boolean(requestResumeForPassed),
3253
+ action_journal_enabled: Boolean(requestResumeForPassed),
2225
3254
  dry_run_request_cv: Boolean(dryRunRequestCv),
2226
3255
  greeting_text: greetingText,
2227
3256
  cv_acquisition_mode: cvAcquisitionMode,
2228
- call_llm_on_image: Boolean(callLlmOnImage),
2229
- screening_mode: normalizedScreeningMode,
2230
- cv_collection_mode: normalizedScreeningMode === "collect_cv",
2231
- llm_configured: Boolean(llmConfig),
3257
+ call_llm_on_image: Boolean(callLlmOnImage),
3258
+ screening_mode: normalizedScreeningMode,
3259
+ cv_collection_mode: normalizedScreeningMode === "collect_cv",
3260
+ llm_configured: Boolean(llmConfig),
2232
3261
  llm_timeout_ms: llmTimeoutMs,
2233
3262
  llm_image_limit: llmImageLimit,
2234
- llm_image_detail: llmImageDetail,
2235
- max_image_pages: maxImagePages,
2236
- image_wheel_delta_y: imageWheelDeltaY,
2237
- list_max_scrolls: listMaxScrolls,
3263
+ llm_image_detail: llmImageDetail,
3264
+ max_image_pages: maxImagePages,
3265
+ image_wheel_delta_y: imageWheelDeltaY,
3266
+ list_max_scrolls: listMaxScrolls,
2238
3267
  list_stable_signature_limit: listStableSignatureLimit,
2239
3268
  list_wheel_delta_y: listWheelDeltaY,
2240
3269
  list_settle_ms: listSettleMs,
@@ -2243,13 +3272,16 @@ export function createChatRunService({
2243
3272
  image_output_dir: imageOutputDir || "",
2244
3273
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2245
3274
  human_behavior_profile: effectiveHumanBehavior.profile,
2246
- human_behavior: effectiveHumanBehavior,
2247
- human_rest_level: effectiveHumanBehavior.restLevel,
2248
- human_rest_enabled: effectiveHumanRestEnabled,
2249
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2250
- human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2251
- human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null
2252
- },
3275
+ human_behavior: effectiveHumanBehavior,
3276
+ human_rest_level: effectiveHumanBehavior.restLevel,
3277
+ human_rest_enabled: effectiveHumanRestEnabled,
3278
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
3279
+ human_rest_per_candidate_min_ms: null,
3280
+ human_rest_per_candidate_max_ms: null,
3281
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
3282
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS : null,
3283
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS : null
3284
+ },
2253
3285
  progress: {
2254
3286
  card_count: 0,
2255
3287
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -2266,74 +3298,83 @@ export function createChatRunService({
2266
3298
  request_skipped: 0,
2267
3299
  context_recoveries: 0,
2268
3300
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2269
- human_behavior_profile: effectiveHumanBehavior.profile,
2270
- human_rest_level: effectiveHumanBehavior.restLevel,
2271
- human_rest_enabled: effectiveHumanRestEnabled,
2272
- human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2273
- human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2274
- human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2275
- human_rest_count: 0,
3301
+ human_behavior_profile: effectiveHumanBehavior.profile,
3302
+ human_rest_level: effectiveHumanBehavior.restLevel,
3303
+ human_rest_enabled: effectiveHumanRestEnabled,
3304
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
3305
+ human_rest_per_candidate_min_ms: null,
3306
+ human_rest_per_candidate_max_ms: null,
3307
+ collect_cv_processing_floor_enabled: collectCvProcessingFloorEnabled,
3308
+ collect_cv_processing_floor_min_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MIN_MS : null,
3309
+ collect_cv_processing_floor_max_ms: collectCvProcessingFloorEnabled ? CHAT_COLLECT_CV_PROCESSING_FLOOR_MAX_MS : null,
3310
+ collect_cv_processing_floor_count: 0,
3311
+ collect_cv_processing_floor_delay_requested_ms: 0,
3312
+ collect_cv_processing_floor_delay_ms: 0,
3313
+ collect_cv_processing_floor_current: null,
3314
+ human_rest_count: 0,
2276
3315
  human_rest_ms: 0,
2277
3316
  last_human_event: null
2278
3317
  },
2279
- checkpoint: {},
2280
- task: async (runControl) => {
2281
- try {
2282
- return await workflow({
2283
- client,
2284
- targetUrl,
2285
- job,
2286
- startFrom,
2287
- criteria,
2288
- maxCandidates,
2289
- targetPassCount,
2290
- processUntilListEnd,
2291
- detailLimit: normalizedDetailLimit,
2292
- detailSource: normalizedDetailSource,
2293
- closeResume,
2294
- requestResumeForPassed,
2295
- dryRunRequestCv,
2296
- greetingText,
2297
- delayMs,
2298
- cardTimeoutMs,
2299
- readyTimeoutMs,
2300
- onlineResumeButtonTimeoutMs,
2301
- resumeDomTimeoutMs,
2302
- maxImagePages,
2303
- imageWheelDeltaY,
2304
- cvAcquisitionMode,
2305
- callLlmOnImage,
2306
- llmConfig,
2307
- llmTimeoutMs,
2308
- llmImageLimit,
2309
- llmImageDetail,
2310
- screeningMode: normalizedScreeningMode,
2311
- listMaxScrolls,
2312
- listStableSignatureLimit,
2313
- listWheelDeltaY,
2314
- listSettleMs,
2315
- listFallbackPoint,
2316
- imageOutputDir,
2317
- humanRestEnabled: effectiveHumanRestEnabled,
2318
- humanBehavior: effectiveHumanBehavior
2319
- }, runControl);
2320
- } catch (error) {
2321
- if (error instanceof RunCanceledError) throw error;
2322
- const finalFailureArtifact = await captureChatFinalFailureArtifact(client, {
2323
- runControl,
2324
- imageOutputDir,
2325
- error
2326
- });
2327
- if (finalFailureArtifact) {
2328
- runControl.checkpoint({
2329
- final_failure_artifact: finalFailureArtifact
2330
- });
2331
- }
2332
- throw error;
2333
- }
2334
- }
2335
- });
2336
- }
3318
+ checkpoint: {},
3319
+ task: async (runControl) => {
3320
+ try {
3321
+ return await workflow({
3322
+ client,
3323
+ targetUrl,
3324
+ job,
3325
+ startFrom,
3326
+ criteria,
3327
+ maxCandidates,
3328
+ targetPassCount,
3329
+ processUntilListEnd,
3330
+ detailLimit: normalizedDetailLimit,
3331
+ detailSource: normalizedDetailSource,
3332
+ closeResume,
3333
+ requestResumeForPassed,
3334
+ dryRunRequestCv,
3335
+ greetingText,
3336
+ delayMs,
3337
+ cardTimeoutMs,
3338
+ readyTimeoutMs,
3339
+ onlineResumeButtonTimeoutMs,
3340
+ resumeDomTimeoutMs,
3341
+ maxImagePages,
3342
+ imageWheelDeltaY,
3343
+ cvAcquisitionMode,
3344
+ callLlmOnImage,
3345
+ llmConfig,
3346
+ llmTimeoutMs,
3347
+ llmImageLimit,
3348
+ llmImageDetail,
3349
+ screeningMode: normalizedScreeningMode,
3350
+ listMaxScrolls,
3351
+ listStableSignatureLimit,
3352
+ listWheelDeltaY,
3353
+ listSettleMs,
3354
+ listFallbackPoint,
3355
+ imageOutputDir,
3356
+ humanRestEnabled: effectiveHumanRestEnabled,
3357
+ humanBehavior: effectiveHumanBehavior,
3358
+ actionJournal: configuredActionJournal || createChatActionJournal(),
3359
+ actionJournalScope
3360
+ }, runControl);
3361
+ } catch (error) {
3362
+ if (error instanceof RunCanceledError) throw error;
3363
+ const finalFailureArtifact = await captureChatFinalFailureArtifact(client, {
3364
+ runControl,
3365
+ imageOutputDir,
3366
+ error
3367
+ });
3368
+ if (finalFailureArtifact) {
3369
+ runControl.checkpoint({
3370
+ final_failure_artifact: finalFailureArtifact
3371
+ });
3372
+ }
3373
+ throw error;
3374
+ }
3375
+ }
3376
+ });
3377
+ }
2337
3378
 
2338
3379
  return {
2339
3380
  startChatRun,