@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
@@ -12,17 +12,17 @@ import {
12
12
  querySelectorAll,
13
13
  sleep
14
14
  } from "../../core/browser/index.js";
15
- import {
16
- buildScreeningCandidateFromDetail,
17
- htmlToText
18
- } from "../../core/screening/index.js";
19
- import {
20
- closeBossAccountRightsBlockingPanel,
21
- findBossAccountRightsBlockingPanel
22
- } from "../common/account-rights-panel.js";
23
- import {
24
- CHAT_ACTIVE_CANDIDATE_SELECTORS,
25
- CHAT_ASK_RESUME_BUTTON_SELECTORS,
15
+ import {
16
+ buildScreeningCandidateFromDetail,
17
+ htmlToText
18
+ } from "../../core/screening/index.js";
19
+ import {
20
+ closeBossAccountRightsBlockingPanel,
21
+ findBossAccountRightsBlockingPanel
22
+ } from "../common/account-rights-panel.js";
23
+ import {
24
+ CHAT_ACTIVE_CANDIDATE_SELECTORS,
25
+ CHAT_ASK_RESUME_BUTTON_SELECTORS,
26
26
  CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
27
27
  CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
28
28
  CHAT_BLOCKING_PANEL_TEXT_QUERIES,
@@ -44,15 +44,16 @@ import {
44
44
  getChatRoots,
45
45
  queryFirstAcrossChatRoots
46
46
  } from "./roots.js";
47
- import {
48
- assertChatShellNotResumeTopLevel,
49
- getChatTopLevelState,
50
- isForbiddenChatResumeTopLevelUrl,
51
- makeForbiddenChatResumeNavigationError
52
- } from "./page-guard.js";
47
+ import {
48
+ assertChatShellNotResumeTopLevel,
49
+ getChatTopLevelState,
50
+ isForbiddenChatResumeTopLevelUrl,
51
+ makeBossSecurityVerificationRequiredError,
52
+ makeForbiddenChatResumeNavigationError
53
+ } from "./page-guard.js";
53
54
 
54
- export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
55
- export const CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE = "CHAT_ONLINE_RESUME_MODAL_NOT_OPEN";
55
+ export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
56
+ export const CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE = "CHAT_ONLINE_RESUME_MODAL_NOT_OPEN";
56
57
 
57
58
  const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
58
59
  ".conversation-main",
@@ -68,7 +69,7 @@ const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
68
69
  ".geek-detail-modal"
69
70
  ]);
70
71
 
71
- const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
72
+ const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
72
73
  ".chat-message-list",
73
74
  ".conversation-editor",
74
75
  ".conversation-main",
@@ -78,8 +79,18 @@ const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
78
79
  ".exchange-tooltip",
79
80
  ".boss-popup__wrapper",
80
81
  ".boss-dialog",
81
- ".dialog-wrap.active"
82
- ]);
82
+ ".dialog-wrap.active"
83
+ ]);
84
+
85
+ const CHAT_REQUEST_RESUME_CONFIRM_PROMPT = "确定向牛人索取简历吗?";
86
+ const CHAT_REQUEST_RESUME_CONFIRM_SCOPE_SELECTORS = Object.freeze([
87
+ ".exchange-tooltip",
88
+ ".boss-popup__wrapper",
89
+ ".boss-dialog",
90
+ ".dialog-wrap.active"
91
+ ]);
92
+ const CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS = 20000;
93
+ const CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS = 250;
83
94
 
84
95
  export function matchesChatProfileNetwork(url) {
85
96
  return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
@@ -129,15 +140,15 @@ export function makeUnsafeChatOnlineResumeLinkError(target = {}, buttonHTML = ""
129
140
  return error;
130
141
  }
131
142
 
132
- export function isUnsafeChatOnlineResumeLinkError(error) {
133
- return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
134
- || /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
135
- }
136
-
137
- export function isChatOnlineResumeModalOpenFailureError(error) {
138
- return error?.code === CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE
139
- || /Chat online resume modal did not open/i.test(String(error?.message || error || ""));
140
- }
143
+ export function isUnsafeChatOnlineResumeLinkError(error) {
144
+ return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
145
+ || /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
146
+ }
147
+
148
+ export function isChatOnlineResumeModalOpenFailureError(error) {
149
+ return error?.code === CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE
150
+ || /Chat online resume modal did not open/i.test(String(error?.message || error || ""));
151
+ }
141
152
 
142
153
  export function createChatProfileNetworkRecorder(client) {
143
154
  const events = [];
@@ -248,9 +259,9 @@ function chatCandidateIdFromAttributes(attributes = {}) {
248
259
  );
249
260
  }
250
261
 
251
- async function hydrateActiveChatCandidate(client, activeCandidate = null) {
252
- if (!activeCandidate?.node_id) return activeCandidate;
253
- let attributes = {};
262
+ async function hydrateActiveChatCandidate(client, activeCandidate = null) {
263
+ if (!activeCandidate?.node_id) return activeCandidate;
264
+ let attributes = {};
254
265
  let outerHTML = "";
255
266
  try {
256
267
  [attributes, outerHTML] = await Promise.all([
@@ -264,25 +275,25 @@ async function hydrateActiveChatCandidate(client, activeCandidate = null) {
264
275
  candidate_id: chatCandidateIdFromAttributes(attributes) || null,
265
276
  label: normalizeDetailText(htmlToText(outerHTML)),
266
277
  outer_html_length: outerHTML.length
267
- };
268
- }
269
-
270
- export async function readChatActiveCandidateState(client) {
271
- const rootState = await getChatRoots(client);
272
- const activeCandidate = await queryFirstAcrossChatRoots(
273
- client,
274
- rootState.roots,
275
- CHAT_ACTIVE_CANDIDATE_SELECTORS
276
- );
277
- return {
278
- roots: rootState.roots,
279
- active_candidate: await hydrateActiveChatCandidate(client, activeCandidate)
280
- };
281
- }
282
-
283
- export async function waitForChatOnlineResumeButton(client, {
284
- timeoutMs = 12000,
285
- intervalMs = 250,
278
+ };
279
+ }
280
+
281
+ export async function readChatActiveCandidateState(client) {
282
+ const rootState = await getChatRoots(client);
283
+ const activeCandidate = await queryFirstAcrossChatRoots(
284
+ client,
285
+ rootState.roots,
286
+ CHAT_ACTIVE_CANDIDATE_SELECTORS
287
+ );
288
+ return {
289
+ roots: rootState.roots,
290
+ active_candidate: await hydrateActiveChatCandidate(client, activeCandidate)
291
+ };
292
+ }
293
+
294
+ export async function waitForChatOnlineResumeButton(client, {
295
+ timeoutMs = 12000,
296
+ intervalMs = 250,
286
297
  expectedCandidateId = ""
287
298
  } = {}) {
288
299
  const started = Date.now();
@@ -389,16 +400,15 @@ function isRequestedResumeText(text = "") {
389
400
  );
390
401
  }
391
402
 
392
- function isResumeRequestSentMessageText(text = "") {
393
- const normalized = normalizeDetailText(text);
394
- return Boolean(
395
- normalized.includes("简历请求已发送")
396
- || normalized.includes("已发送简历")
397
- || normalized.includes("已求简历")
398
- || normalized.includes("已索要简历")
399
- || normalized.includes("已发送")
400
- );
401
- }
403
+ function isResumeRequestSentMessageText(text = "") {
404
+ const normalized = normalizeDetailText(text);
405
+ return Boolean(
406
+ normalized.includes("简历请求已发送")
407
+ || normalized.includes("已发送简历")
408
+ || normalized.includes("已求简历")
409
+ || normalized.includes("已索要简历")
410
+ );
411
+ }
402
412
 
403
413
  function countTextOccurrences(text = "", needle = "") {
404
414
  if (!needle) return 0;
@@ -413,11 +423,11 @@ function countTextOccurrences(text = "", needle = "") {
413
423
  return count;
414
424
  }
415
425
 
416
- function countResumeRequestSentMessageMarkers(lines = []) {
417
- const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历", "已发送"];
418
- return lines.reduce((total, line) => (
419
- total + markers.reduce((lineTotal, marker) => (
420
- lineTotal + countTextOccurrences(line, marker)
426
+ function countResumeRequestSentMessageMarkers(lines = []) {
427
+ const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历"];
428
+ return lines.reduce((total, line) => (
429
+ total + markers.reduce((lineTotal, marker) => (
430
+ lineTotal + countTextOccurrences(line, marker)
421
431
  ), 0)
422
432
  ), 0);
423
433
  }
@@ -463,17 +473,14 @@ function isAttachmentResumeTarget(target = {}) {
463
473
  || /resume-btn-file/i.test(String(target.attributes?.class || target.selector || ""));
464
474
  }
465
475
 
466
- function isConfirmText(text = "") {
467
- const normalized = normalizeDetailText(text);
468
- return Boolean(
469
- normalized === "确定"
470
- || normalized === "确认"
471
- || normalized === "提交"
472
- || normalized === "继续"
473
- || normalized.includes("确定")
474
- || normalized.includes("确认")
475
- );
476
- }
476
+ function isRequestResumeConfirmPrompt(text = "") {
477
+ return normalizeDetailText(text).includes(CHAT_REQUEST_RESUME_CONFIRM_PROMPT);
478
+ }
479
+
480
+ function isExactRequestResumeConfirmText(text = "") {
481
+ const normalized = normalizeDetailText(text);
482
+ return normalized === "确定" || normalized === "确认";
483
+ }
477
484
 
478
485
  function isSendText(text = "") {
479
486
  const normalized = normalizeDetailText(text);
@@ -534,9 +541,9 @@ async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
534
541
  return null;
535
542
  }
536
543
 
537
- async function resolveScopedRoots(client, roots = [], selectors = [], {
538
- fallbackToRoots = true
539
- } = {}) {
544
+ async function resolveScopedRoots(client, roots = [], selectors = [], {
545
+ fallbackToRoots = true
546
+ } = {}) {
540
547
  const scoped = [];
541
548
  const seen = new Set();
542
549
  for (const root of roots) {
@@ -559,9 +566,56 @@ async function resolveScopedRoots(client, roots = [], selectors = [], {
559
566
  }
560
567
  }
561
568
  }
562
- if (scoped.length || !fallbackToRoots) return scoped;
563
- return roots;
564
- }
569
+ if (scoped.length || !fallbackToRoots) return scoped;
570
+ return roots;
571
+ }
572
+
573
+ async function findAnchoredChatRequestResumeConfirmTarget(client, roots = []) {
574
+ const promptRoots = await resolveScopedRoots(
575
+ client,
576
+ roots,
577
+ CHAT_REQUEST_RESUME_CONFIRM_SCOPE_SELECTORS,
578
+ { fallbackToRoots: false }
579
+ );
580
+ let lastPrompt = null;
581
+ let lastPromptCandidate = null;
582
+ let lastTargetCandidate = null;
583
+ for (const promptRoot of promptRoots) {
584
+ const prompt = await readTarget(
585
+ client,
586
+ promptRoot,
587
+ "request-resume-confirm-prompt",
588
+ promptRoot.nodeId
589
+ );
590
+ lastPromptCandidate = prompt;
591
+ if (!prompt.visible || !isRequestResumeConfirmPrompt(prompt.label)) continue;
592
+ lastPrompt = prompt;
593
+ for (const selector of CHAT_CONFIRM_REQUEST_RESUME_SELECTORS) {
594
+ const nodeIds = await querySelectorAll(client, promptRoot.nodeId, selector);
595
+ for (const nodeId of nodeIds) {
596
+ const target = await readTarget(client, promptRoot, selector, nodeId);
597
+ lastTargetCandidate = target;
598
+ if (!target.visible || target.disabled || !isExactRequestResumeConfirmText(target.label)) {
599
+ continue;
600
+ }
601
+ return {
602
+ prompt,
603
+ prompt_candidate: lastPromptCandidate,
604
+ target,
605
+ target_candidate: lastTargetCandidate,
606
+ discovery_mode: "exact_request_prompt"
607
+ };
608
+ }
609
+ }
610
+ }
611
+ return {
612
+ prompt: lastPrompt,
613
+ prompt_candidate: lastPromptCandidate,
614
+ target: null,
615
+ target_candidate: lastTargetCandidate,
616
+ discovery_mode: lastPrompt ? "exact_request_prompt" : null
617
+ };
618
+ }
565
619
 
566
620
  export async function selectChatPrimaryLabel(client, {
567
621
  label = "全部",
@@ -758,29 +812,29 @@ export async function quickChatResumeModalOpenProbe(client, {
758
812
  };
759
813
  }
760
814
 
761
- export async function findChatBlockingPanel(client, {
762
- textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
763
- } = {}) {
764
- const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
765
- return panel.open
766
- ? { ...panel, reason: "blocking_panel_text_visible" }
767
- : panel;
768
- }
769
-
770
- export async function closeChatBlockingPanels(client, {
771
- attemptsLimit = 2,
772
- closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
773
- textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
774
- } = {}) {
775
- return closeBossAccountRightsBlockingPanel(client, {
776
- attemptsLimit,
777
- closeSelectors,
778
- resolveRoots: getChatRoots,
779
- textQueries
780
- });
781
- }
782
-
783
- export async function readChatResumeHtml(client, resumeState) {
815
+ export async function findChatBlockingPanel(client, {
816
+ textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
817
+ } = {}) {
818
+ const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
819
+ return panel.open
820
+ ? { ...panel, reason: "blocking_panel_text_visible" }
821
+ : panel;
822
+ }
823
+
824
+ export async function closeChatBlockingPanels(client, {
825
+ attemptsLimit = 2,
826
+ closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
827
+ textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
828
+ } = {}) {
829
+ return closeBossAccountRightsBlockingPanel(client, {
830
+ attemptsLimit,
831
+ closeSelectors,
832
+ resolveRoots: getChatRoots,
833
+ textQueries
834
+ });
835
+ }
836
+
837
+ export async function readChatResumeHtml(client, resumeState) {
784
838
  let popupHTML = "";
785
839
  let contentHTML = "";
786
840
  let resumeIframeHTML = "";
@@ -1001,14 +1055,14 @@ export async function openChatOnlineResume(client, {
1001
1055
  }
1002
1056
  }
1003
1057
 
1004
- const error = new Error("Chat online resume modal did not open");
1005
- error.code = CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE;
1006
- error.retryable = true;
1007
- error.attempts = attempts;
1008
- throw error;
1009
- }
1058
+ const error = new Error("Chat online resume modal did not open");
1059
+ error.code = CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE;
1060
+ error.retryable = true;
1061
+ error.attempts = attempts;
1062
+ throw error;
1063
+ }
1010
1064
 
1011
- export async function readChatConversationReadyState(client) {
1065
+ export async function readChatConversationReadyState(client) {
1012
1066
  const rootState = await getChatRoots(client);
1013
1067
  const scopedControlRoots = await resolveScopedRoots(
1014
1068
  client,
@@ -1082,60 +1136,1000 @@ export async function readChatConversationReadyState(client) {
1082
1136
  blocking_panel_open: blockingPanelOpen,
1083
1137
  blocking_panel: blockingPanelOpen ? blockingPanel : null,
1084
1138
  panels_closed: !resumeModalOpen && !blockingPanelOpen
1085
- };
1086
- }
1087
-
1088
- export async function setChatEditorMessage(client, message, {
1089
- timeoutMs = 8000
1090
- } = {}) {
1091
- const started = Date.now();
1092
- let lastState = null;
1093
- while (Date.now() - started <= timeoutMs) {
1094
- const state = await readChatConversationReadyState(client);
1095
- lastState = state;
1096
- if (state.editor?.node_id) {
1097
- try {
1098
- if (state.editor.center) {
1099
- await clickPoint(client, state.editor.center.x, state.editor.center.y);
1100
- } else {
1101
- await clickNodeCenter(client, state.editor.node_id, { scrollIntoView: true });
1102
- }
1103
- await sleep(120);
1104
- await clearFocusedInput(client);
1105
- await sleep(80);
1106
- await insertText(client, message);
1107
- await sleep(250);
1108
- const afterState = await readChatConversationReadyState(client);
1109
- const editorText = normalizeDetailText(afterState.editor?.label || "");
1110
- if (editorText.includes(normalizeDetailText(message))) {
1111
- return {
1112
- ok: true,
1113
- value: editorText,
1114
- editor: afterState.editor || state.editor
1115
- };
1116
- }
1117
- lastState = {
1118
- ...afterState,
1119
- editor_message_mismatch: true,
1120
- editor_text: editorText
1121
- };
1122
- } catch (error) {
1123
- if (!isRecoverableNodeError(error)) throw error;
1124
- lastState = {
1125
- ...state,
1126
- recoverable_error: error?.message || String(error),
1139
+ };
1140
+ }
1141
+
1142
+ function readAccessibilityBooleanProperty(node, propertyName) {
1143
+ for (const property of node?.properties || []) {
1144
+ if (property?.name !== propertyName) continue;
1145
+ if (typeof property?.value?.value === "boolean") return property.value.value;
1146
+ }
1147
+ return null;
1148
+ }
1149
+
1150
+ async function readFreshChatEditorTarget(client) {
1151
+ const rootState = await getChatRoots(client);
1152
+ const scopedControlRoots = await resolveScopedRoots(
1153
+ client,
1154
+ rootState.roots,
1155
+ CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS,
1156
+ { fallbackToRoots: false }
1157
+ );
1158
+ const controlRoots = scopedControlRoots.length ? scopedControlRoots : rootState.roots;
1159
+ return findVisibleMatchingTarget(
1160
+ client,
1161
+ controlRoots,
1162
+ CHAT_EDITOR_SELECTORS,
1163
+ () => true
1164
+ );
1165
+ }
1166
+
1167
+ async function readChatEditorFocusState(client, editor) {
1168
+ if (!editor?.node_id) {
1169
+ return {
1170
+ available: false,
1171
+ focused: false,
1172
+ reason: "editor_node_missing"
1173
+ };
1174
+ }
1175
+ if (typeof client?.Accessibility?.getPartialAXTree !== "function") {
1176
+ return {
1177
+ available: false,
1178
+ focused: false,
1179
+ reason: "accessibility_unavailable",
1180
+ editor_node_id: editor.node_id
1181
+ };
1182
+ }
1183
+ try {
1184
+ const result = await client.Accessibility.getPartialAXTree({
1185
+ nodeId: editor.node_id,
1186
+ fetchRelatives: false
1187
+ });
1188
+ const node = result?.nodes?.[0] || null;
1189
+ if (!node) {
1190
+ return {
1191
+ available: false,
1192
+ focused: false,
1193
+ reason: "accessibility_node_missing",
1194
+ editor_node_id: editor.node_id
1195
+ };
1196
+ }
1197
+ return {
1198
+ available: true,
1199
+ focused: readAccessibilityBooleanProperty(node, "focused") === true,
1200
+ focusable: readAccessibilityBooleanProperty(node, "focusable"),
1201
+ role: node?.role?.value || "",
1202
+ backend_dom_node_id: node?.backendDOMNodeId || null,
1203
+ editor_node_id: editor.node_id
1204
+ };
1205
+ } catch (error) {
1206
+ return {
1207
+ available: false,
1208
+ focused: false,
1209
+ reason: "accessibility_read_failed",
1210
+ read_error: error?.message || String(error),
1211
+ editor_node_id: editor.node_id
1212
+ };
1213
+ }
1214
+ }
1215
+
1216
+ const CHAT_EDITOR_FOCUS_SETTLE_LIMIT_MS = 600;
1217
+ const CHAT_EDITOR_FOCUS_POLL_INTERVAL_MS = 100;
1218
+
1219
+ function canChatEditorFocusStillSettle(focus) {
1220
+ if (focus?.focused) return false;
1221
+ if (focus?.available && focus?.focusable === true && !focus?.read_error) return true;
1222
+ if (focus?.reason === "editor_node_missing" || focus?.reason === "accessibility_node_missing") {
1223
+ return true;
1224
+ }
1225
+ return Boolean(focus?.read_error && isRecoverableNodeError(focus.read_error));
1226
+ }
1227
+
1228
+ async function waitForFreshChatEditorFocus(client, {
1229
+ initialDelayMs = 0,
1230
+ settleLimitMs = CHAT_EDITOR_FOCUS_SETTLE_LIMIT_MS,
1231
+ pollIntervalMs = CHAT_EDITOR_FOCUS_POLL_INTERVAL_MS
1232
+ } = {}) {
1233
+ const startedAt = Date.now();
1234
+ const observations = [];
1235
+ let editor = null;
1236
+ let focus = {
1237
+ available: false,
1238
+ focused: false,
1239
+ reason: "focus_not_read"
1240
+ };
1241
+ let firstRead = true;
1242
+ while (true) {
1243
+ try {
1244
+ editor = await readFreshChatEditorTarget(client);
1245
+ focus = await readChatEditorFocusState(client, editor);
1246
+ } catch (error) {
1247
+ if (!isRecoverableNodeError(error)) throw error;
1248
+ editor = null;
1249
+ focus = {
1250
+ available: false,
1251
+ focused: false,
1252
+ reason: "recoverable_node_error",
1253
+ read_error: error?.message || String(error)
1254
+ };
1255
+ }
1256
+ observations.push({
1257
+ elapsed_ms: Math.max(0, Date.now() - startedAt),
1258
+ editor_node_id: editor?.node_id || null,
1259
+ available: focus.available,
1260
+ focused: focus.focused,
1261
+ focusable: focus.focusable ?? null,
1262
+ role: focus.role || null,
1263
+ backend_dom_node_id: focus.backend_dom_node_id || null,
1264
+ reason: focus.reason || null,
1265
+ read_error: focus.read_error || null
1266
+ });
1267
+ const elapsedMs = Date.now() - startedAt;
1268
+ if (
1269
+ focus.focused
1270
+ || !canChatEditorFocusStillSettle(focus)
1271
+ || elapsedMs >= settleLimitMs
1272
+ ) {
1273
+ break;
1274
+ }
1275
+ const delayMs = firstRead && initialDelayMs > 0
1276
+ ? initialDelayMs
1277
+ : pollIntervalMs;
1278
+ firstRead = false;
1279
+ await sleep(Math.min(delayMs, Math.max(1, settleLimitMs - elapsedMs)));
1280
+ }
1281
+ return {
1282
+ editor,
1283
+ focus,
1284
+ observations,
1285
+ poll_count: Math.max(0, observations.length - 1),
1286
+ elapsed_ms: observations.at(-1)?.elapsed_ms || 0,
1287
+ settled_after_poll: Boolean(focus.focused && observations.length > 1)
1288
+ };
1289
+ }
1290
+
1291
+ async function recoverChatEditorFocusWithDomFocus(client, editor, focus) {
1292
+ const backendNodeId = Number(focus?.backend_dom_node_id);
1293
+ const editorNodeId = Number(editor?.node_id);
1294
+ if (
1295
+ typeof client?.DOM?.focus !== "function"
1296
+ || typeof client?.DOM?.describeNode !== "function"
1297
+ || !Number.isInteger(editorNodeId)
1298
+ || editorNodeId <= 0
1299
+ || focus?.focused
1300
+ || focus?.available !== true
1301
+ || focus?.focusable !== true
1302
+ || focus?.read_error
1303
+ || !Number.isInteger(backendNodeId)
1304
+ || backendNodeId <= 0
1305
+ ) {
1306
+ return {
1307
+ attempted: false,
1308
+ dispatched: false,
1309
+ editor,
1310
+ focus,
1311
+ settle: null
1312
+ };
1313
+ }
1314
+
1315
+ let focusCallAttempted = false;
1316
+ try {
1317
+ const freshEditor = await readFreshChatEditorTarget(client);
1318
+ const freshEditorNodeId = Number(freshEditor?.node_id);
1319
+ const axEditorNodeId = Number(focus?.editor_node_id);
1320
+ if (
1321
+ !Number.isInteger(freshEditorNodeId)
1322
+ || freshEditorNodeId <= 0
1323
+ || freshEditorNodeId !== editorNodeId
1324
+ || axEditorNodeId !== editorNodeId
1325
+ ) {
1326
+ return {
1327
+ attempted: true,
1328
+ dispatched: false,
1329
+ target_kind: "backend_node_id",
1330
+ target_id: backendNodeId,
1331
+ editor: freshEditor || editor,
1332
+ focus,
1333
+ settle: null,
1334
+ pre_focus_editor_node_id: editorNodeId,
1335
+ fresh_editor_node_id: Number.isInteger(freshEditorNodeId) ? freshEditorNodeId : null,
1336
+ ax_editor_node_id: Number.isInteger(axEditorNodeId) ? axEditorNodeId : null,
1337
+ focus_call_attempted: false,
1338
+ binding_failed: true,
1339
+ pre_focus_binding_verified: false,
1340
+ post_focus_binding_verified: false,
1341
+ error: "fresh_editor_node_binding_mismatch"
1342
+ };
1343
+ }
1344
+
1345
+ const described = await client.DOM.describeNode({ nodeId: freshEditorNodeId });
1346
+ const describedBackendNodeId = Number(described?.node?.backendNodeId);
1347
+ if (
1348
+ !Number.isInteger(describedBackendNodeId)
1349
+ || describedBackendNodeId <= 0
1350
+ || describedBackendNodeId !== backendNodeId
1351
+ ) {
1352
+ return {
1353
+ attempted: true,
1354
+ dispatched: false,
1355
+ target_kind: "backend_node_id",
1356
+ target_id: backendNodeId,
1357
+ editor: freshEditor,
1358
+ focus,
1359
+ settle: null,
1360
+ pre_focus_editor_node_id: editorNodeId,
1361
+ fresh_editor_node_id: freshEditorNodeId,
1362
+ ax_editor_node_id: axEditorNodeId,
1363
+ described_backend_dom_node_id: Number.isInteger(describedBackendNodeId)
1364
+ ? describedBackendNodeId
1365
+ : null,
1366
+ focus_call_attempted: false,
1367
+ binding_failed: true,
1368
+ pre_focus_binding_verified: false,
1369
+ post_focus_binding_verified: false,
1370
+ error: "editor_backend_node_binding_mismatch"
1371
+ };
1372
+ }
1373
+
1374
+ focusCallAttempted = true;
1375
+ await client.DOM.focus({ backendNodeId });
1376
+ const settle = await waitForFreshChatEditorFocus(client, {
1377
+ initialDelayMs: 80
1378
+ });
1379
+ const postFocusEditorNodeId = Number(settle.editor?.node_id);
1380
+ const postFocusAxEditorNodeId = Number(settle.focus?.editor_node_id);
1381
+ const postFocusBackendNodeId = Number(settle.focus?.backend_dom_node_id);
1382
+ const postFocusBindingVerified = Boolean(
1383
+ Number.isInteger(postFocusEditorNodeId)
1384
+ && postFocusEditorNodeId > 0
1385
+ && postFocusAxEditorNodeId === postFocusEditorNodeId
1386
+ && postFocusBackendNodeId === backendNodeId
1387
+ );
1388
+ const postFocusVerified = postFocusBindingVerified && settle.focus?.focused === true;
1389
+ const verifiedFocus = postFocusVerified
1390
+ ? settle.focus
1391
+ : {
1392
+ ...settle.focus,
1393
+ focused: false,
1394
+ reason: postFocusBindingVerified
1395
+ ? "native_focus_not_verified"
1396
+ : "native_focus_post_binding_mismatch"
1397
+ };
1398
+ return {
1399
+ attempted: true,
1400
+ dispatched: true,
1401
+ target_kind: "backend_node_id",
1402
+ target_id: backendNodeId,
1403
+ editor: settle.editor || editor,
1404
+ focus: verifiedFocus,
1405
+ settle: {
1406
+ ...settle,
1407
+ focus: verifiedFocus
1408
+ },
1409
+ pre_focus_editor_node_id: editorNodeId,
1410
+ fresh_editor_node_id: freshEditorNodeId,
1411
+ ax_editor_node_id: axEditorNodeId,
1412
+ described_backend_dom_node_id: describedBackendNodeId,
1413
+ focus_call_attempted: true,
1414
+ binding_failed: !postFocusVerified,
1415
+ pre_focus_binding_verified: true,
1416
+ post_focus_editor_node_id: Number.isInteger(postFocusEditorNodeId)
1417
+ ? postFocusEditorNodeId
1418
+ : null,
1419
+ post_focus_ax_editor_node_id: Number.isInteger(postFocusAxEditorNodeId)
1420
+ ? postFocusAxEditorNodeId
1421
+ : null,
1422
+ post_focus_backend_dom_node_id: Number.isInteger(postFocusBackendNodeId)
1423
+ ? postFocusBackendNodeId
1424
+ : null,
1425
+ post_focus_binding_verified: postFocusBindingVerified,
1426
+ post_focus_verified: postFocusVerified,
1427
+ error: postFocusVerified
1428
+ ? null
1429
+ : postFocusBindingVerified
1430
+ ? "native_focus_not_verified"
1431
+ : "post_focus_backend_node_binding_mismatch"
1432
+ };
1433
+ } catch (error) {
1434
+ if (!focusCallAttempted && !isRecoverableNodeError(error)) throw error;
1435
+ return {
1436
+ attempted: true,
1437
+ dispatched: false,
1438
+ target_kind: "backend_node_id",
1439
+ target_id: backendNodeId,
1440
+ editor,
1441
+ focus,
1442
+ settle: null,
1443
+ focus_call_attempted: focusCallAttempted,
1444
+ binding_failed: focusCallAttempted,
1445
+ error: error?.message || String(error)
1446
+ };
1447
+ }
1448
+ }
1449
+
1450
+ async function focusChatEditorForInput(client, initialEditor, {
1451
+ deterministic = false,
1452
+ attemptsLimit = 3
1453
+ } = {}) {
1454
+ let editor = initialEditor || null;
1455
+ let lastState = null;
1456
+ const attempts = [];
1457
+ let nativeFocusAttempted = false;
1458
+ const limit = Math.max(1, Number(attemptsLimit) || 1);
1459
+ for (let attempt = 1; attempt <= limit; attempt += 1) {
1460
+ if (!editor?.node_id) {
1461
+ editor = await readFreshChatEditorTarget(client);
1462
+ lastState = { editor };
1463
+ }
1464
+ if (!editor?.node_id) {
1465
+ attempts.push({
1466
+ attempt,
1467
+ editor_node_id: null,
1468
+ focused: false,
1469
+ reason: "editor_not_found"
1470
+ });
1471
+ if (attempt < limit) await sleep(160);
1472
+ continue;
1473
+ }
1474
+
1475
+ let clickResult = null;
1476
+ try {
1477
+ const useDeterministicClick = deterministic || attempt > 1;
1478
+ if (editor.center) {
1479
+ clickResult = await clickPoint(
1480
+ client,
1481
+ editor.center.x,
1482
+ editor.center.y,
1483
+ useDeterministicClick ? DETERMINISTIC_CLICK_OPTIONS : undefined
1484
+ );
1485
+ } else {
1486
+ clickResult = await clickNodeCenter(client, editor.node_id, {
1487
+ scrollIntoView: true,
1488
+ ...(useDeterministicClick ? DETERMINISTIC_CLICK_OPTIONS : {})
1489
+ });
1490
+ }
1491
+ const clickedEditor = editor;
1492
+ // Boss can remount the contenteditable as a direct consequence of focus.
1493
+ // Frontend DOM node ids are session/document-epoch scoped, so never ask
1494
+ // Accessibility about the pre-click id. Reacquire the editor first.
1495
+ const clickFocusSettle = await waitForFreshChatEditorFocus(client, {
1496
+ initialDelayMs: attempt === 1 ? 120 : 180
1497
+ });
1498
+ let verificationEditor = clickFocusSettle.editor;
1499
+ lastState = { editor: verificationEditor };
1500
+ let focus = clickFocusSettle.focus;
1501
+ let focusSettle = clickFocusSettle;
1502
+ const nativeFocusRecovery = nativeFocusAttempted
1503
+ ? {
1504
+ attempted: false,
1505
+ dispatched: false,
1506
+ editor: verificationEditor,
1507
+ focus,
1508
+ settle: null
1509
+ }
1510
+ : await recoverChatEditorFocusWithDomFocus(
1511
+ client,
1512
+ verificationEditor,
1513
+ focus
1514
+ );
1515
+ nativeFocusAttempted = nativeFocusAttempted || nativeFocusRecovery.attempted;
1516
+ if (nativeFocusRecovery.attempted) {
1517
+ verificationEditor = nativeFocusRecovery.editor || verificationEditor;
1518
+ focus = nativeFocusRecovery.focus || focus;
1519
+ focusSettle = nativeFocusRecovery.settle || focusSettle;
1520
+ lastState = { editor: verificationEditor };
1521
+ }
1522
+ attempts.push({
1523
+ attempt,
1524
+ clicked_editor_node_id: clickedEditor.node_id,
1525
+ editor_node_id: verificationEditor?.node_id || null,
1526
+ editor_reacquired: Boolean(verificationEditor?.node_id),
1527
+ editor_node_changed: Boolean(
1528
+ verificationEditor?.node_id
1529
+ && verificationEditor.node_id !== clickedEditor.node_id
1530
+ ),
1531
+ editor_selector: verificationEditor?.selector || clickedEditor.selector || null,
1532
+ editor_trace_id: verificationEditor?.attributes?.traceid || clickedEditor.attributes?.traceid || null,
1533
+ click_mode: clickResult?.mode || clickResult?.click_result?.mode || null,
1534
+ focus_available: focus.available,
1535
+ focused: focus.focused,
1536
+ focusable: focus.focusable ?? null,
1537
+ focus_role: focus.role || null,
1538
+ focus_backend_dom_node_id: focus.backend_dom_node_id || null,
1539
+ focus_reason: focus.reason || null,
1540
+ focus_read_error: focus.read_error || null,
1541
+ focus_poll_count: focusSettle.poll_count,
1542
+ focus_settle_elapsed_ms: focusSettle.elapsed_ms,
1543
+ focus_settled_after_poll: focusSettle.settled_after_poll,
1544
+ focus_observations: focusSettle.observations,
1545
+ click_focus_poll_count: clickFocusSettle.poll_count,
1546
+ click_focus_settle_elapsed_ms: clickFocusSettle.elapsed_ms,
1547
+ click_focus_settled_after_poll: clickFocusSettle.settled_after_poll,
1548
+ click_focus_observations: clickFocusSettle.observations,
1549
+ native_focus_attempted: nativeFocusRecovery.attempted,
1550
+ native_focus_dispatched: nativeFocusRecovery.dispatched,
1551
+ native_focus_target_kind: nativeFocusRecovery.target_kind || null,
1552
+ native_focus_target_id: nativeFocusRecovery.target_id || null,
1553
+ native_focus_error: nativeFocusRecovery.error || null,
1554
+ native_focus_call_attempted: nativeFocusRecovery.focus_call_attempted === true,
1555
+ native_focus_binding_failed: nativeFocusRecovery.binding_failed === true,
1556
+ native_focus_pre_editor_node_id: nativeFocusRecovery.pre_focus_editor_node_id || null,
1557
+ native_focus_fresh_editor_node_id: nativeFocusRecovery.fresh_editor_node_id || null,
1558
+ native_focus_ax_editor_node_id: nativeFocusRecovery.ax_editor_node_id || null,
1559
+ native_focus_described_backend_dom_node_id: nativeFocusRecovery.described_backend_dom_node_id || null,
1560
+ native_focus_pre_binding_verified: nativeFocusRecovery.pre_focus_binding_verified === true,
1561
+ native_focus_post_editor_node_id: nativeFocusRecovery.post_focus_editor_node_id || null,
1562
+ native_focus_post_ax_editor_node_id: nativeFocusRecovery.post_focus_ax_editor_node_id || null,
1563
+ native_focus_post_backend_dom_node_id: nativeFocusRecovery.post_focus_backend_dom_node_id || null,
1564
+ native_focus_post_binding_verified: nativeFocusRecovery.post_focus_binding_verified === true,
1565
+ native_focus_post_verified: nativeFocusRecovery.post_focus_verified === true,
1566
+ native_focus_poll_count: nativeFocusRecovery.settle?.poll_count ?? null,
1567
+ native_focus_settle_elapsed_ms: nativeFocusRecovery.settle?.elapsed_ms ?? null,
1568
+ native_focus_settled_after_poll: nativeFocusRecovery.settle?.settled_after_poll ?? null,
1569
+ native_focus_observations: nativeFocusRecovery.settle?.observations || []
1570
+ });
1571
+ if (nativeFocusRecovery.binding_failed) {
1572
+ return {
1573
+ ok: false,
1574
+ terminal_binding_failure: true,
1575
+ editor: verificationEditor,
1576
+ focus,
1577
+ attempts,
1578
+ state: lastState,
1579
+ terminal_focus_binding_failure: nativeFocusRecovery.focus_call_attempted === true,
1580
+ focus_binding_error: nativeFocusRecovery.error || null
1581
+ };
1582
+ }
1583
+ if (focus.focused) {
1584
+ return {
1585
+ ok: true,
1586
+ editor: verificationEditor,
1587
+ focus,
1588
+ native_focus_backend_node_id: nativeFocusRecovery.dispatched
1589
+ && nativeFocusRecovery.pre_focus_binding_verified
1590
+ && nativeFocusRecovery.post_focus_binding_verified
1591
+ ? nativeFocusRecovery.target_id
1592
+ : null,
1593
+ attempts,
1594
+ state: lastState
1595
+ };
1596
+ }
1597
+ editor = focus.read_error && isRecoverableNodeError(focus.read_error)
1598
+ ? null
1599
+ : verificationEditor;
1600
+ } catch (error) {
1601
+ if (!isRecoverableNodeError(error)) throw error;
1602
+ attempts.push({
1603
+ attempt,
1604
+ editor_node_id: editor.node_id,
1605
+ editor_selector: editor.selector || null,
1606
+ editor_trace_id: editor.attributes?.traceid || null,
1607
+ focused: false,
1608
+ recoverable_error: error?.message || String(error)
1609
+ });
1610
+ editor = null;
1611
+ }
1612
+
1613
+ if (attempt < limit) await sleep(160);
1614
+ }
1615
+ return {
1616
+ ok: false,
1617
+ editor,
1618
+ focus: attempts[attempts.length - 1] || null,
1619
+ attempts,
1620
+ state: lastState
1621
+ };
1622
+ }
1623
+
1624
+ async function verifyChatEditorBackendBinding(client, editor, focus, expectedBackendNodeId) {
1625
+ const expected = Number(expectedBackendNodeId);
1626
+ if (!Number.isInteger(expected) || expected <= 0) {
1627
+ return {
1628
+ required: false,
1629
+ verified: true
1630
+ };
1631
+ }
1632
+ const editorNodeId = Number(editor?.node_id);
1633
+ const axEditorNodeId = Number(focus?.editor_node_id);
1634
+ const axBackendNodeId = Number(focus?.backend_dom_node_id);
1635
+ const base = {
1636
+ required: true,
1637
+ expected_backend_dom_node_id: expected,
1638
+ editor_node_id: Number.isInteger(editorNodeId) ? editorNodeId : null,
1639
+ ax_editor_node_id: Number.isInteger(axEditorNodeId) ? axEditorNodeId : null,
1640
+ ax_backend_dom_node_id: Number.isInteger(axBackendNodeId) ? axBackendNodeId : null
1641
+ };
1642
+ if (
1643
+ focus?.focused !== true
1644
+ || !Number.isInteger(editorNodeId)
1645
+ || editorNodeId <= 0
1646
+ || axEditorNodeId !== editorNodeId
1647
+ || axBackendNodeId !== expected
1648
+ || typeof client?.DOM?.describeNode !== "function"
1649
+ ) {
1650
+ return {
1651
+ ...base,
1652
+ verified: false,
1653
+ error: "editor_backend_node_binding_mismatch"
1654
+ };
1655
+ }
1656
+ try {
1657
+ const described = await client.DOM.describeNode({ nodeId: editorNodeId });
1658
+ const describedBackendNodeId = Number(described?.node?.backendNodeId);
1659
+ const verified = Number.isInteger(describedBackendNodeId)
1660
+ && describedBackendNodeId > 0
1661
+ && describedBackendNodeId === expected;
1662
+ return {
1663
+ ...base,
1664
+ described_backend_dom_node_id: Number.isInteger(describedBackendNodeId)
1665
+ ? describedBackendNodeId
1666
+ : null,
1667
+ verified,
1668
+ error: verified ? null : "editor_backend_node_binding_mismatch"
1669
+ };
1670
+ } catch (error) {
1671
+ return {
1672
+ ...base,
1673
+ verified: false,
1674
+ error: error?.message || String(error)
1675
+ };
1676
+ }
1677
+ }
1678
+
1679
+ export async function setChatEditorMessage(client, message, {
1680
+ timeoutMs = 8000
1681
+ } = {}) {
1682
+ const expectedText = normalizeDetailText(message);
1683
+ const started = Date.now();
1684
+ let lastState = null;
1685
+ let editorFound = false;
1686
+ const attempts = [];
1687
+ while (Date.now() - started <= timeoutMs) {
1688
+ const state = await readChatConversationReadyState(client);
1689
+ lastState = state;
1690
+ if (state.editor?.node_id) {
1691
+ editorFound = true;
1692
+ try {
1693
+ const focusResult = await focusChatEditorForInput(client, state.editor, {
1694
+ deterministic: false,
1695
+ attemptsLimit: 3
1696
+ });
1697
+ const focusedEditor = focusResult.editor || state.editor;
1698
+ if (!focusResult.ok) {
1699
+ const observedText = normalizeDetailText(focusedEditor?.label || "");
1700
+ attempts.push({
1701
+ stage: "normal",
1702
+ editor_node_id: focusedEditor?.node_id || state.editor.node_id,
1703
+ editor_selector: focusedEditor?.selector || state.editor.selector || null,
1704
+ editor_trace_id: focusedEditor?.attributes?.traceid || state.editor.attributes?.traceid || null,
1705
+ input_mode: null,
1706
+ chunk_count: 0,
1707
+ expected_length: expectedText.length,
1708
+ observed_length: observedText.length,
1709
+ exact_match: false,
1710
+ focus_verified: false,
1711
+ focus_attempts: focusResult.attempts,
1712
+ terminal_focus_binding_failure: focusResult.terminal_focus_binding_failure === true,
1713
+ focus_binding_error: focusResult.focus_binding_error || null
1714
+ });
1715
+ lastState = {
1716
+ ...(focusResult.state || state),
1717
+ editor_focus_unverified: true,
1718
+ terminal_focus_binding_failure: focusResult.terminal_focus_binding_failure === true,
1719
+ focus_binding_error: focusResult.focus_binding_error || null,
1720
+ editor_text_length: observedText.length
1721
+ };
1722
+ if (focusResult.terminal_focus_binding_failure) {
1723
+ return {
1724
+ ok: false,
1725
+ error: "CHAT_EDITOR_MESSAGE_MISMATCH",
1726
+ editor_found: true,
1727
+ attempts,
1728
+ deterministic_fallback_attempted: false,
1729
+ state: lastState
1730
+ };
1731
+ }
1732
+ break;
1733
+ }
1734
+ const clearSkipped = normalizeDetailText(focusedEditor.label || "") === "";
1735
+ const nativeFocusBackendNodeId = Number(focusResult.native_focus_backend_node_id);
1736
+ let preClearFocusSettle = null;
1737
+ let preClearBinding = {
1738
+ required: false,
1739
+ verified: true
1740
+ };
1741
+ if (Number.isInteger(nativeFocusBackendNodeId) && nativeFocusBackendNodeId > 0) {
1742
+ preClearFocusSettle = await waitForFreshChatEditorFocus(client, {
1743
+ initialDelayMs: 80
1744
+ });
1745
+ preClearBinding = await verifyChatEditorBackendBinding(
1746
+ client,
1747
+ preClearFocusSettle.editor,
1748
+ preClearFocusSettle.focus,
1749
+ nativeFocusBackendNodeId
1750
+ );
1751
+ if (!preClearBinding.verified) {
1752
+ attempts.push({
1753
+ stage: "normal",
1754
+ editor_node_id: preClearFocusSettle.editor?.node_id || null,
1755
+ editor_selector: preClearFocusSettle.editor?.selector || focusedEditor.selector || null,
1756
+ editor_trace_id: preClearFocusSettle.editor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
1757
+ input_mode: null,
1758
+ chunk_count: 0,
1759
+ expected_length: expectedText.length,
1760
+ observed_length: normalizeDetailText(focusedEditor.label || "").length,
1761
+ exact_match: false,
1762
+ focus_verified: false,
1763
+ terminal_focus_binding_failure: true,
1764
+ focus_binding_error: preClearBinding.error || null,
1765
+ focus_attempts: focusResult.attempts,
1766
+ pre_clear_focus: preClearFocusSettle.focus,
1767
+ pre_clear_focus_observations: preClearFocusSettle.observations,
1768
+ pre_clear_binding: preClearBinding,
1769
+ clear_skipped: true
1770
+ });
1771
+ lastState = {
1772
+ editor: preClearFocusSettle.editor,
1773
+ terminal_focus_binding_failure: true,
1774
+ focus_binding_error: preClearBinding.error || null,
1775
+ editor_focus_unverified: true
1776
+ };
1777
+ return {
1778
+ ok: false,
1779
+ error: "CHAT_EDITOR_MESSAGE_MISMATCH",
1780
+ editor_found: true,
1781
+ attempts,
1782
+ deterministic_fallback_attempted: false,
1783
+ state: lastState
1784
+ };
1785
+ }
1786
+ }
1787
+ if (!clearSkipped) await clearFocusedInput(client);
1788
+ const preInsertFocusSettle = clearSkipped && preClearFocusSettle
1789
+ ? preClearFocusSettle
1790
+ : await waitForFreshChatEditorFocus(client, {
1791
+ initialDelayMs: 80
1792
+ });
1793
+ const preInsertEditor = preInsertFocusSettle.editor;
1794
+ const preInsertState = { editor: preInsertEditor };
1795
+ const preInsertFocus = preInsertFocusSettle.focus;
1796
+ const preInsertBinding = clearSkipped && preClearFocusSettle
1797
+ ? preClearBinding
1798
+ : await verifyChatEditorBackendBinding(
1799
+ client,
1800
+ preInsertEditor,
1801
+ preInsertFocus,
1802
+ nativeFocusBackendNodeId
1803
+ );
1804
+ if (!preInsertFocus.focused || !preInsertBinding.verified) {
1805
+ attempts.push({
1806
+ stage: "normal",
1807
+ editor_node_id: preInsertEditor?.node_id || null,
1808
+ editor_node_changed_before_insert: Boolean(
1809
+ preInsertEditor?.node_id
1810
+ && preInsertEditor.node_id !== focusedEditor.node_id
1811
+ ),
1812
+ editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
1813
+ editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
1814
+ input_mode: null,
1815
+ chunk_count: 0,
1816
+ expected_length: expectedText.length,
1817
+ observed_length: normalizeDetailText(focusedEditor.label || "").length,
1818
+ exact_match: false,
1819
+ focus_verified: false,
1820
+ focus_lost_before_insert: true,
1821
+ terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
1822
+ focus_binding_error: preInsertBinding.error || null,
1823
+ focus_attempts: focusResult.attempts,
1824
+ pre_insert_focus: preInsertFocus,
1825
+ pre_insert_binding: preInsertBinding,
1826
+ pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
1827
+ pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
1828
+ pre_insert_focus_observations: preInsertFocusSettle.observations,
1829
+ clear_skipped: clearSkipped
1830
+ });
1831
+ lastState = {
1832
+ ...preInsertState,
1833
+ editor_focus_lost_before_insert: true,
1834
+ terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
1835
+ focus_binding_error: preInsertBinding.error || null
1836
+ };
1837
+ if (preInsertBinding.required && !preInsertBinding.verified) {
1838
+ return {
1839
+ ok: false,
1840
+ error: "CHAT_EDITOR_MESSAGE_MISMATCH",
1841
+ editor_found: true,
1842
+ attempts,
1843
+ deterministic_fallback_attempted: false,
1844
+ state: lastState
1845
+ };
1846
+ }
1847
+ break;
1848
+ }
1849
+ const inputResult = await insertText(client, message);
1850
+ await sleep(250);
1851
+ const afterState = await readChatConversationReadyState(client);
1852
+ const editorText = normalizeDetailText(afterState.editor?.label || "");
1853
+ const exactMatch = editorText === expectedText;
1854
+ attempts.push({
1855
+ stage: "normal",
1856
+ editor_node_id: preInsertEditor?.node_id || focusedEditor.node_id,
1857
+ editor_node_changed_before_insert: Boolean(
1858
+ preInsertEditor?.node_id
1859
+ && preInsertEditor.node_id !== focusedEditor.node_id
1860
+ ),
1861
+ editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
1862
+ editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
1863
+ input_mode: inputResult?.mode || null,
1864
+ chunk_count: Number(inputResult?.chunk_count || 0),
1865
+ expected_length: expectedText.length,
1866
+ observed_length: editorText.length,
1867
+ exact_match: exactMatch,
1868
+ focus_verified: true,
1869
+ focus_attempts: focusResult.attempts,
1870
+ pre_insert_focus: preInsertFocus,
1871
+ pre_insert_binding: preInsertBinding,
1872
+ pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
1873
+ pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
1874
+ pre_insert_focus_observations: preInsertFocusSettle.observations,
1875
+ clear_skipped: clearSkipped,
1876
+ read_error: afterState.editor?.read_error || null
1877
+ });
1878
+ if (exactMatch) {
1879
+ return {
1880
+ ok: true,
1881
+ value: editorText,
1882
+ editor: afterState.editor || state.editor,
1883
+ attempts,
1884
+ deterministic_fallback_attempted: false
1885
+ };
1886
+ }
1887
+ lastState = {
1888
+ ...afterState,
1889
+ editor_message_mismatch: true,
1890
+ editor_text_length: editorText.length
1891
+ };
1892
+ break;
1893
+ } catch (error) {
1894
+ if (!isRecoverableNodeError(error)) throw error;
1895
+ attempts.push({
1896
+ stage: "normal",
1897
+ editor_node_id: state.editor.node_id,
1898
+ editor_selector: state.editor.selector || null,
1899
+ editor_trace_id: state.editor.attributes?.traceid || null,
1900
+ expected_length: expectedText.length,
1901
+ exact_match: false,
1902
+ recoverable_error: error?.message || String(error)
1903
+ });
1904
+ lastState = {
1905
+ ...state,
1906
+ recoverable_error: error?.message || String(error),
1127
1907
  recoverable_phase: "set_editor_message"
1128
1908
  };
1129
1909
  }
1130
- }
1131
- await sleep(250);
1132
- }
1133
- return {
1134
- ok: false,
1135
- error: "CHAT_EDITOR_NOT_FOUND",
1136
- state: lastState
1137
- };
1138
- }
1910
+ }
1911
+ await sleep(250);
1912
+ }
1913
+
1914
+ // A chat transition can briefly steal focus or remount the contenteditable
1915
+ // while human-style chunked input is in progress. No outbound action has
1916
+ // started yet, so one freshly reacquired, deterministic editor-only retry is
1917
+ // safe. The message still must be read back exactly before callers may send.
1918
+ let deterministicFallbackAttempted = false;
1919
+ const fallbackState = await readChatConversationReadyState(client);
1920
+ lastState = fallbackState || lastState;
1921
+ if (fallbackState?.editor?.node_id) {
1922
+ editorFound = true;
1923
+ deterministicFallbackAttempted = true;
1924
+ try {
1925
+ const focusResult = await focusChatEditorForInput(client, fallbackState.editor, {
1926
+ deterministic: true,
1927
+ attemptsLimit: 3
1928
+ });
1929
+ const focusedEditor = focusResult.editor || fallbackState.editor;
1930
+ if (!focusResult.ok) {
1931
+ const observedText = normalizeDetailText(focusedEditor?.label || "");
1932
+ attempts.push({
1933
+ stage: "deterministic_fallback",
1934
+ editor_node_id: focusedEditor?.node_id || fallbackState.editor.node_id,
1935
+ editor_selector: focusedEditor?.selector || fallbackState.editor.selector || null,
1936
+ editor_trace_id: focusedEditor?.attributes?.traceid || fallbackState.editor.attributes?.traceid || null,
1937
+ input_mode: null,
1938
+ chunk_count: 0,
1939
+ expected_length: expectedText.length,
1940
+ observed_length: observedText.length,
1941
+ exact_match: false,
1942
+ focus_verified: false,
1943
+ focus_attempts: focusResult.attempts
1944
+ });
1945
+ lastState = {
1946
+ ...(focusResult.state || fallbackState),
1947
+ editor_focus_unverified: true,
1948
+ editor_text_length: observedText.length
1949
+ };
1950
+ } else {
1951
+ const clearSkipped = normalizeDetailText(focusedEditor.label || "") === "";
1952
+ const nativeFocusBackendNodeId = Number(focusResult.native_focus_backend_node_id);
1953
+ let preClearFocusSettle = null;
1954
+ let preClearBinding = {
1955
+ required: false,
1956
+ verified: true
1957
+ };
1958
+ if (Number.isInteger(nativeFocusBackendNodeId) && nativeFocusBackendNodeId > 0) {
1959
+ preClearFocusSettle = await waitForFreshChatEditorFocus(client, {
1960
+ initialDelayMs: 80
1961
+ });
1962
+ preClearBinding = await verifyChatEditorBackendBinding(
1963
+ client,
1964
+ preClearFocusSettle.editor,
1965
+ preClearFocusSettle.focus,
1966
+ nativeFocusBackendNodeId
1967
+ );
1968
+ if (!preClearBinding.verified) {
1969
+ attempts.push({
1970
+ stage: "deterministic_fallback",
1971
+ editor_node_id: preClearFocusSettle.editor?.node_id || null,
1972
+ editor_selector: preClearFocusSettle.editor?.selector || focusedEditor.selector || null,
1973
+ editor_trace_id: preClearFocusSettle.editor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
1974
+ input_mode: null,
1975
+ chunk_count: 0,
1976
+ expected_length: expectedText.length,
1977
+ observed_length: normalizeDetailText(focusedEditor.label || "").length,
1978
+ exact_match: false,
1979
+ focus_verified: false,
1980
+ terminal_focus_binding_failure: true,
1981
+ focus_binding_error: preClearBinding.error || null,
1982
+ focus_attempts: focusResult.attempts,
1983
+ pre_clear_focus: preClearFocusSettle.focus,
1984
+ pre_clear_focus_observations: preClearFocusSettle.observations,
1985
+ pre_clear_binding: preClearBinding,
1986
+ clear_skipped: true
1987
+ });
1988
+ lastState = {
1989
+ editor: preClearFocusSettle.editor,
1990
+ terminal_focus_binding_failure: true,
1991
+ focus_binding_error: preClearBinding.error || null,
1992
+ editor_focus_unverified: true
1993
+ };
1994
+ return {
1995
+ ok: false,
1996
+ error: "CHAT_EDITOR_MESSAGE_MISMATCH",
1997
+ editor_found: true,
1998
+ attempts,
1999
+ deterministic_fallback_attempted: true,
2000
+ state: lastState
2001
+ };
2002
+ }
2003
+ }
2004
+ if (!clearSkipped) await clearFocusedInput(client);
2005
+ const preInsertFocusSettle = clearSkipped && preClearFocusSettle
2006
+ ? preClearFocusSettle
2007
+ : await waitForFreshChatEditorFocus(client, {
2008
+ initialDelayMs: 80
2009
+ });
2010
+ const preInsertEditor = preInsertFocusSettle.editor;
2011
+ const preInsertState = { editor: preInsertEditor };
2012
+ const preInsertFocus = preInsertFocusSettle.focus;
2013
+ const preInsertBinding = clearSkipped && preClearFocusSettle
2014
+ ? preClearBinding
2015
+ : await verifyChatEditorBackendBinding(
2016
+ client,
2017
+ preInsertEditor,
2018
+ preInsertFocus,
2019
+ nativeFocusBackendNodeId
2020
+ );
2021
+ if (!preInsertFocus.focused || !preInsertBinding.verified) {
2022
+ attempts.push({
2023
+ stage: "deterministic_fallback",
2024
+ editor_node_id: preInsertEditor?.node_id || null,
2025
+ editor_node_changed_before_insert: Boolean(
2026
+ preInsertEditor?.node_id
2027
+ && preInsertEditor.node_id !== focusedEditor.node_id
2028
+ ),
2029
+ editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
2030
+ editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
2031
+ input_mode: null,
2032
+ chunk_count: 0,
2033
+ expected_length: expectedText.length,
2034
+ observed_length: normalizeDetailText(focusedEditor.label || "").length,
2035
+ exact_match: false,
2036
+ focus_verified: false,
2037
+ focus_lost_before_insert: true,
2038
+ terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
2039
+ focus_binding_error: preInsertBinding.error || null,
2040
+ focus_attempts: focusResult.attempts,
2041
+ pre_insert_focus: preInsertFocus,
2042
+ pre_insert_binding: preInsertBinding,
2043
+ pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
2044
+ pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
2045
+ pre_insert_focus_observations: preInsertFocusSettle.observations,
2046
+ clear_skipped: clearSkipped
2047
+ });
2048
+ lastState = {
2049
+ ...preInsertState,
2050
+ editor_focus_lost_before_insert: true,
2051
+ terminal_focus_binding_failure: preInsertBinding.required && !preInsertBinding.verified,
2052
+ focus_binding_error: preInsertBinding.error || null
2053
+ };
2054
+ } else {
2055
+ const inputResult = await insertText(client, message, {
2056
+ humanTextEntryEnabled: false
2057
+ });
2058
+ await sleep(350);
2059
+ const afterState = await readChatConversationReadyState(client);
2060
+ const editorText = normalizeDetailText(afterState.editor?.label || "");
2061
+ const exactMatch = editorText === expectedText;
2062
+ attempts.push({
2063
+ stage: "deterministic_fallback",
2064
+ editor_node_id: preInsertEditor?.node_id || focusedEditor.node_id,
2065
+ editor_node_changed_before_insert: Boolean(
2066
+ preInsertEditor?.node_id
2067
+ && preInsertEditor.node_id !== focusedEditor.node_id
2068
+ ),
2069
+ editor_selector: preInsertEditor?.selector || focusedEditor.selector || null,
2070
+ editor_trace_id: preInsertEditor?.attributes?.traceid || focusedEditor.attributes?.traceid || null,
2071
+ input_mode: inputResult?.mode || null,
2072
+ chunk_count: Number(inputResult?.chunk_count || 0),
2073
+ expected_length: expectedText.length,
2074
+ observed_length: editorText.length,
2075
+ exact_match: exactMatch,
2076
+ focus_verified: true,
2077
+ focus_attempts: focusResult.attempts,
2078
+ pre_insert_focus: preInsertFocus,
2079
+ pre_insert_binding: preInsertBinding,
2080
+ pre_insert_focus_poll_count: preInsertFocusSettle.poll_count,
2081
+ pre_insert_focus_settle_elapsed_ms: preInsertFocusSettle.elapsed_ms,
2082
+ pre_insert_focus_observations: preInsertFocusSettle.observations,
2083
+ clear_skipped: clearSkipped,
2084
+ read_error: afterState.editor?.read_error || null
2085
+ });
2086
+ if (exactMatch) {
2087
+ return {
2088
+ ok: true,
2089
+ value: editorText,
2090
+ editor: afterState.editor || preInsertEditor || focusedEditor,
2091
+ attempts,
2092
+ deterministic_fallback_attempted: true,
2093
+ recovery: {
2094
+ mode: "deterministic_reacquired_editor"
2095
+ }
2096
+ };
2097
+ }
2098
+ lastState = {
2099
+ ...afterState,
2100
+ editor_message_mismatch: true,
2101
+ editor_text_length: editorText.length
2102
+ };
2103
+ }
2104
+ }
2105
+ } catch (error) {
2106
+ if (!isRecoverableNodeError(error)) throw error;
2107
+ attempts.push({
2108
+ stage: "deterministic_fallback",
2109
+ editor_node_id: fallbackState.editor.node_id,
2110
+ editor_selector: fallbackState.editor.selector || null,
2111
+ editor_trace_id: fallbackState.editor.attributes?.traceid || null,
2112
+ expected_length: expectedText.length,
2113
+ exact_match: false,
2114
+ recoverable_error: error?.message || String(error)
2115
+ });
2116
+ lastState = {
2117
+ ...fallbackState,
2118
+ recoverable_error: error?.message || String(error),
2119
+ recoverable_phase: "set_editor_message_deterministic_fallback"
2120
+ };
2121
+ }
2122
+ }
2123
+
2124
+ return {
2125
+ ok: false,
2126
+ error: editorFound ? "CHAT_EDITOR_MESSAGE_MISMATCH" : "CHAT_EDITOR_NOT_FOUND",
2127
+ editor_found: editorFound,
2128
+ attempts,
2129
+ deterministic_fallback_attempted: deterministicFallbackAttempted,
2130
+ state: lastState
2131
+ };
2132
+ }
1139
2133
 
1140
2134
  export async function sendChatMessage(client, expectedText = "", {
1141
2135
  timeoutMs = 8000,
@@ -1194,21 +2188,21 @@ export async function sendChatMessage(client, expectedText = "", {
1194
2188
  };
1195
2189
  }
1196
2190
 
1197
- export async function clickChatAskResume(client, {
1198
- timeoutMs = 8000,
1199
- settleMs = 700,
1200
- skipWhenAttachmentResumeAvailable = true
1201
- } = {}) {
2191
+ export async function clickChatAskResume(client, {
2192
+ timeoutMs = 8000,
2193
+ settleMs = 700,
2194
+ skipWhenAttachmentResumeAvailable = true
2195
+ } = {}) {
1202
2196
  const started = Date.now();
1203
2197
  let lastState = null;
1204
2198
  let lastDisabledAskResume = null;
1205
2199
  while (Date.now() - started <= timeoutMs) {
1206
2200
  const state = await readChatConversationReadyState(client);
1207
2201
  lastState = state;
1208
- if (skipWhenAttachmentResumeAvailable && state.attachment_resume_enabled) {
1209
- return {
1210
- ok: true,
1211
- already_requested: true,
2202
+ if (skipWhenAttachmentResumeAvailable && state.attachment_resume_enabled) {
2203
+ return {
2204
+ ok: true,
2205
+ already_requested: true,
1212
2206
  attachment_resume_available: true,
1213
2207
  control: state.attachment_resume
1214
2208
  };
@@ -1247,12 +2241,12 @@ export async function clickChatAskResume(client, {
1247
2241
  }
1248
2242
  await sleep(250);
1249
2243
  }
1250
- if (lastDisabledAskResume) {
1251
- return {
1252
- ok: false,
1253
- already_requested: true,
1254
- request_pending: true,
1255
- error: "ASK_RESUME_BUTTON_DISABLED",
2244
+ if (lastDisabledAskResume) {
2245
+ return {
2246
+ ok: false,
2247
+ already_requested: false,
2248
+ request_pending: false,
2249
+ error: "ASK_RESUME_BUTTON_DISABLED",
1256
2250
  control: lastDisabledAskResume,
1257
2251
  state: lastState
1258
2252
  };
@@ -1264,63 +2258,161 @@ export async function clickChatAskResume(client, {
1264
2258
  };
1265
2259
  }
1266
2260
 
1267
- export async function clickChatConfirmRequestResume(client, {
1268
- timeoutMs = 8000,
1269
- settleMs = 900
1270
- } = {}) {
1271
- const started = Date.now();
1272
- let lastTarget = null;
1273
- let lastState = null;
1274
- while (Date.now() - started <= timeoutMs) {
1275
- lastState = await readChatConversationReadyState(client);
1276
- const rootState = await getChatRoots(client);
1277
- const confirmRoots = await resolveScopedRoots(
1278
- client,
1279
- rootState.roots,
1280
- CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS
1281
- );
1282
- const target = await findVisibleMatchingTarget(
1283
- client,
1284
- confirmRoots,
1285
- CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
1286
- (item) => isConfirmText(item.label) && !item.disabled
1287
- );
1288
- lastTarget = target;
1289
- if (target?.node_id) {
1290
- try {
1291
- if (target.center) {
1292
- await clickPoint(client, target.center.x, target.center.y);
1293
- } else {
1294
- await clickNodeCenter(client, target.node_id, { scrollIntoView: true });
1295
- }
1296
- if (settleMs > 0) await sleep(settleMs);
1297
- const afterState = await readChatConversationReadyState(client);
1298
- return {
1299
- confirmed: true,
1300
- assumed_requested: Boolean(afterState.already_requested_resume),
1301
- control: target,
1302
- state: afterState
1303
- };
1304
- } catch (error) {
1305
- if (!isRecoverableNodeError(error)) throw error;
1306
- lastTarget = {
1307
- ...target,
1308
- recoverable_error: error?.message || String(error),
1309
- recoverable_phase: "confirm_request_resume_click"
1310
- };
1311
- }
1312
- }
1313
- await sleep(250);
1314
- }
1315
- return {
1316
- confirmed: false,
1317
- error: "CONFIRM_BUTTON_NOT_FOUND",
1318
- control: lastTarget,
1319
- state: lastState
1320
- };
1321
- }
1322
-
1323
- export async function getChatResumeRequestMessageState(client) {
2261
+ export function resolveChatConfirmResumeTimeoutMs(value) {
2262
+ const hasExplicitValue = value !== null
2263
+ && value !== undefined
2264
+ && !(typeof value === "string" && value.trim() === "");
2265
+ if (!hasExplicitValue) return CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS;
2266
+ const parsedValue = Number(value);
2267
+ return Number.isFinite(parsedValue)
2268
+ ? Math.max(0, parsedValue)
2269
+ : CHAT_REQUEST_RESUME_CONFIRM_DEFAULT_TIMEOUT_MS;
2270
+ }
2271
+
2272
+ export async function clickChatConfirmRequestResume(client, {
2273
+ timeoutMs = null,
2274
+ settleMs = 900,
2275
+ pollIntervalMs = CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS
2276
+ } = {}) {
2277
+ const effectiveTimeoutMs = resolveChatConfirmResumeTimeoutMs(timeoutMs);
2278
+ const parsedPollIntervalMs = Number(pollIntervalMs);
2279
+ const effectivePollIntervalMs = Number.isFinite(parsedPollIntervalMs)
2280
+ ? Math.max(1, parsedPollIntervalMs)
2281
+ : CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS;
2282
+ const started = Date.now();
2283
+ let lastPrompt = null;
2284
+ let lastTarget = null;
2285
+ let lastState = null;
2286
+ let discoveryMode = null;
2287
+ let pollCount = 0;
2288
+ let firstPromptObservedElapsedMs = null;
2289
+ let firstTargetObservedElapsedMs = null;
2290
+ let lastPromptReadError = null;
2291
+ let lastTargetReadError = null;
2292
+ let clickAttempted = false;
2293
+ let clickDispatched = false;
2294
+ const diagnostics = () => ({
2295
+ timeout_ms: effectiveTimeoutMs,
2296
+ poll_interval_ms: effectivePollIntervalMs,
2297
+ poll_count: pollCount,
2298
+ elapsed_ms: Math.max(0, Date.now() - started),
2299
+ discovery_mode: discoveryMode,
2300
+ prompt_observed: Boolean(lastPrompt),
2301
+ first_prompt_observed_elapsed_ms: firstPromptObservedElapsedMs,
2302
+ last_prompt_read_error: lastPromptReadError,
2303
+ target_observed: Boolean(lastTarget),
2304
+ first_target_observed_elapsed_ms: firstTargetObservedElapsedMs,
2305
+ last_target_read_error: lastTargetReadError,
2306
+ click_attempted: clickAttempted,
2307
+ click_dispatched: clickDispatched
2308
+ });
2309
+
2310
+ while (true) {
2311
+ pollCount += 1;
2312
+ lastState = await readChatConversationReadyState(client);
2313
+ if (lastState.already_requested_resume || lastState.attachment_resume_enabled) {
2314
+ return {
2315
+ confirmed: true,
2316
+ assumed_requested: true,
2317
+ reason: lastState.attachment_resume_enabled
2318
+ ? "attachment_resume_available"
2319
+ : "request_state_observed",
2320
+ control: lastState.requested_resume || lastState.attachment_resume || null,
2321
+ prompt: lastPrompt,
2322
+ state: lastState,
2323
+ ...diagnostics()
2324
+ };
2325
+ }
2326
+ const rootState = await getChatRoots(client);
2327
+ let discovered;
2328
+ try {
2329
+ discovered = await findAnchoredChatRequestResumeConfirmTarget(
2330
+ client,
2331
+ rootState.roots
2332
+ );
2333
+ } catch (error) {
2334
+ if (!isRecoverableNodeError(error)) throw error;
2335
+ lastTargetReadError = error?.message || String(error);
2336
+ discovered = {
2337
+ prompt: null,
2338
+ prompt_candidate: null,
2339
+ target: null,
2340
+ target_candidate: null,
2341
+ discovery_mode: null
2342
+ };
2343
+ }
2344
+ lastPromptReadError = discovered.prompt_candidate?.read_error || lastPromptReadError;
2345
+ lastTargetReadError = discovered.target_candidate?.read_error || lastTargetReadError;
2346
+ if (discovered.prompt) {
2347
+ lastPrompt = discovered.prompt;
2348
+ discoveryMode = discovered.discovery_mode;
2349
+ if (firstPromptObservedElapsedMs === null) {
2350
+ firstPromptObservedElapsedMs = Math.max(0, Date.now() - started);
2351
+ }
2352
+ }
2353
+ lastTarget = discovered.target;
2354
+ if (lastTarget && firstTargetObservedElapsedMs === null) {
2355
+ firstTargetObservedElapsedMs = Math.max(0, Date.now() - started);
2356
+ }
2357
+ if (lastTarget?.node_id) {
2358
+ clickAttempted = true;
2359
+ try {
2360
+ if (lastTarget.center) {
2361
+ await clickPoint(client, lastTarget.center.x, lastTarget.center.y);
2362
+ } else {
2363
+ await clickNodeCenter(client, lastTarget.node_id, { scrollIntoView: true });
2364
+ }
2365
+ clickDispatched = true;
2366
+ if (settleMs > 0) await sleep(settleMs);
2367
+ const afterState = await readChatConversationReadyState(client);
2368
+ return {
2369
+ confirmed: true,
2370
+ assumed_requested: Boolean(afterState.already_requested_resume),
2371
+ control: lastTarget,
2372
+ prompt: lastPrompt,
2373
+ state: afterState,
2374
+ ...diagnostics()
2375
+ };
2376
+ } catch (error) {
2377
+ if (!isRecoverableNodeError(error)) throw error;
2378
+ const control = {
2379
+ ...lastTarget,
2380
+ recoverable_error: error?.message || String(error),
2381
+ recoverable_phase: clickDispatched
2382
+ ? "confirm_request_resume_post_click_state"
2383
+ : "confirm_request_resume_click"
2384
+ };
2385
+ return {
2386
+ confirmed: false,
2387
+ outcome_unknown: true,
2388
+ error: clickDispatched
2389
+ ? "CONFIRM_CLICK_OUTCOME_UNKNOWN"
2390
+ : "CONFIRM_CLICK_FAILED",
2391
+ control,
2392
+ prompt: lastPrompt,
2393
+ state: lastState,
2394
+ ...diagnostics()
2395
+ };
2396
+ }
2397
+ }
2398
+ const elapsedMs = Date.now() - started;
2399
+ if (elapsedMs >= effectiveTimeoutMs) break;
2400
+ await sleep(Math.min(
2401
+ effectivePollIntervalMs,
2402
+ Math.max(1, effectiveTimeoutMs - elapsedMs)
2403
+ ));
2404
+ }
2405
+ return {
2406
+ confirmed: false,
2407
+ error: "CONFIRM_BUTTON_NOT_FOUND",
2408
+ control: null,
2409
+ prompt: lastPrompt,
2410
+ state: lastState,
2411
+ ...diagnostics()
2412
+ };
2413
+ }
2414
+
2415
+ export async function getChatResumeRequestMessageState(client) {
1324
2416
  const rootState = await getChatRoots(client);
1325
2417
  let messageRoot = null;
1326
2418
  for (const root of rootState.roots) {
@@ -1357,65 +2449,152 @@ export async function getChatResumeRequestMessageState(client) {
1357
2449
  last_resume_attachment_text: attachmentMatching[attachmentMatching.length - 1] || "",
1358
2450
  last_success_text: matching[matching.length - 1] || attachmentMatching[attachmentMatching.length - 1] || "",
1359
2451
  recent: lines.slice(-12)
1360
- };
1361
- }
1362
-
1363
- export async function waitForChatResumeRequestMessage(client, {
2452
+ };
2453
+ }
2454
+
2455
+ export async function readChatRequestVerificationEvidence(client) {
2456
+ // Both readers reacquire the document root. Running them concurrently can
2457
+ // invalidate the first reader's frontend node IDs when the second
2458
+ // DOM.getDocument call refreshes Chrome's node-id namespace.
2459
+ const messageState = await getChatResumeRequestMessageState(client);
2460
+ const readyState = await readChatConversationReadyState(client);
2461
+ return {
2462
+ message_state: messageState,
2463
+ ready_state: readyState
2464
+ };
2465
+ }
2466
+
2467
+ const CHAT_GREETING_DELIVERY_PREFIX_PATTERN = /^(?:送达|已送达|已读|未读)\s*/u;
2468
+
2469
+ export function isExactChatGreetingMessageText(value = "", expectedValue = "") {
2470
+ const line = normalizeDetailText(value);
2471
+ const expected = normalizeDetailText(expectedValue);
2472
+ if (!line || !expected) return false;
2473
+ if (line === expected) return true;
2474
+ return normalizeDetailText(line.replace(CHAT_GREETING_DELIVERY_PREFIX_PATTERN, "")) === expected;
2475
+ }
2476
+
2477
+ export async function getChatGreetingMessageState(client, greetingText = "") {
2478
+ const expected = normalizeDetailText(greetingText);
2479
+ const rootState = await getChatRoots(client);
2480
+ let messageRoot = null;
2481
+ for (const root of rootState.roots) {
2482
+ for (const selector of CHAT_MESSAGE_LIST_SELECTORS) {
2483
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
2484
+ if (nodeIds.length) {
2485
+ messageRoot = {
2486
+ root,
2487
+ selector,
2488
+ node_id: nodeIds[0]
2489
+ };
2490
+ break;
2491
+ }
2492
+ }
2493
+ if (messageRoot) break;
2494
+ }
2495
+ if (!messageRoot?.node_id || !expected) {
2496
+ return {
2497
+ ok: false,
2498
+ selector: messageRoot?.selector || null,
2499
+ expected_text: expected,
2500
+ exact_count: 0,
2501
+ recent: []
2502
+ };
2503
+ }
2504
+ let text = "";
2505
+ try {
2506
+ text = htmlToText(await getOuterHTML(client, messageRoot.node_id));
2507
+ } catch {}
2508
+ const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
2509
+ return {
2510
+ ok: Boolean(text),
2511
+ selector: messageRoot.selector,
2512
+ expected_text: expected,
2513
+ exact_count: lines.filter((line) => isExactChatGreetingMessageText(line, expected)).length,
2514
+ recent: lines.slice(-12)
2515
+ };
2516
+ }
2517
+
2518
+ export async function waitForChatResumeRequestMessage(client, {
1364
2519
  baselineCount = 0,
1365
2520
  baselineResumeAttachmentCount = 0,
1366
2521
  timeoutMs = 6500,
1367
2522
  intervalMs = 260
1368
- } = {}) {
1369
- const started = Date.now();
1370
- let state = null;
1371
- while (Date.now() - started <= timeoutMs) {
1372
- state = await getChatResumeRequestMessageState(client);
1373
- const observed = (
1374
- state.count > baselineCount
2523
+ } = {}) {
2524
+ const started = Date.now();
2525
+ let state = null;
2526
+ while (true) {
2527
+ state = await getChatResumeRequestMessageState(client);
2528
+ const observed = (
2529
+ state.count > baselineCount
1375
2530
  || state.resume_attachment_count > baselineResumeAttachmentCount
1376
2531
  );
1377
2532
  if (observed) {
1378
2533
  return {
1379
2534
  observed: true,
1380
2535
  elapsed_ms: Date.now() - started,
1381
- state
1382
- };
1383
- }
1384
- await sleep(intervalMs);
1385
- }
1386
- return {
2536
+ state
2537
+ };
2538
+ }
2539
+ const elapsedMs = Date.now() - started;
2540
+ if (elapsedMs >= timeoutMs) break;
2541
+ await sleep(Math.min(intervalMs, Math.max(0, timeoutMs - elapsedMs)));
2542
+ }
2543
+ return {
1387
2544
  observed: false,
1388
2545
  elapsed_ms: Date.now() - started,
1389
2546
  state
1390
- };
1391
- }
1392
-
1393
- export async function requestChatResumeForPassedCandidate(client, {
1394
- greetingText = "Hi同学,能麻烦发下简历吗?",
1395
- maxAttempts = 3,
1396
- askResumeTimeoutMs = 8000,
1397
- dryRun = false,
1398
- skipWhenAttachmentResumeAvailable = true
1399
- } = {}) {
1400
- const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
1401
- const initialState = await readChatConversationReadyState(client);
1402
- if (skipWhenAttachmentResumeAvailable && initialState.attachment_resume_enabled) {
1403
- return {
1404
- requested: false,
1405
- skipped: true,
1406
- reason: "attachment_resume_already_available",
1407
- initial_state: initialState
1408
- };
1409
- }
1410
- if (initialState.already_requested_resume) {
1411
- return {
1412
- requested: false,
1413
- skipped: true,
1414
- reason: "resume_request_already_pending",
1415
- initial_state: initialState
1416
- };
1417
- }
1418
- if (dryRun) {
2547
+ };
2548
+ }
2549
+
2550
+ export function resolveChatRequestVerificationTimeoutMs(value, maxAttempts = 3) {
2551
+ const parsedAttempts = Number(maxAttempts);
2552
+ const effectiveAttempts = Number.isFinite(parsedAttempts)
2553
+ ? Math.max(0, parsedAttempts)
2554
+ : 3;
2555
+ const fallbackMs = Math.max(6500, Math.min(20000, effectiveAttempts * 3250));
2556
+ const hasExplicitValue = value !== null
2557
+ && value !== undefined
2558
+ && !(typeof value === "string" && value.trim() === "");
2559
+ if (!hasExplicitValue) return fallbackMs;
2560
+ const parsedValue = Number(value);
2561
+ return Number.isFinite(parsedValue)
2562
+ ? Math.max(0, parsedValue)
2563
+ : fallbackMs;
2564
+ }
2565
+
2566
+ export async function requestChatResumeForPassedCandidate(client, {
2567
+ greetingText = "Hi同学,能麻烦发下简历吗?",
2568
+ maxAttempts = 3,
2569
+ askResumeTimeoutMs = 8000,
2570
+ confirmResumeTimeoutMs = null,
2571
+ confirmResumePollIntervalMs = CHAT_REQUEST_RESUME_CONFIRM_POLL_INTERVAL_MS,
2572
+ requestVerificationTimeoutMs = null,
2573
+ requestFinalVerificationSettleMs = 260,
2574
+ dryRun = false,
2575
+ skipWhenAttachmentResumeAvailable = true,
2576
+ skipGreeting = false,
2577
+ actionTransition = null
2578
+ } = {}) {
2579
+ const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
2580
+ const initialState = await readChatConversationReadyState(client);
2581
+ if (skipWhenAttachmentResumeAvailable && initialState.attachment_resume_enabled) {
2582
+ return {
2583
+ requested: false,
2584
+ skipped: true,
2585
+ reason: "attachment_resume_already_available",
2586
+ initial_state: initialState
2587
+ };
2588
+ }
2589
+ if (initialState.already_requested_resume) {
2590
+ return {
2591
+ requested: false,
2592
+ skipped: true,
2593
+ reason: "resume_request_already_pending",
2594
+ initial_state: initialState
2595
+ };
2596
+ }
2597
+ if (dryRun) {
1419
2598
  return {
1420
2599
  requested: false,
1421
2600
  skipped: false,
@@ -1436,123 +2615,227 @@ export async function requestChatResumeForPassedCandidate(client, {
1436
2615
  close_before_greeting: closeBeforeGreeting
1437
2616
  };
1438
2617
  }
1439
- const editorState = await setChatEditorMessage(client, effectiveGreetingText);
1440
- if (!editorState.ok) {
1441
- throw new Error("CHAT_EDITOR_MESSAGE_MISMATCH");
1442
- }
1443
- const sendResult = await sendChatMessage(client, effectiveGreetingText);
1444
- if (!sendResult.sent) {
1445
- throw new Error(`CHAT_GREETING_SEND_FAILED:${sendResult.error || sendResult.method || "unknown"}`);
1446
- }
1447
-
1448
- const attempts = [];
1449
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1450
- const before = await getChatResumeRequestMessageState(client);
1451
- const askResult = await clickChatAskResume(client, {
1452
- timeoutMs: askResumeTimeoutMs,
1453
- skipWhenAttachmentResumeAvailable
1454
- });
1455
- let confirmResult = {
1456
- confirmed: false,
1457
- assumed_requested: Boolean(askResult.already_requested),
1458
- skipped: true,
1459
- reason: askResult.attachment_resume_available
1460
- ? "attachment_resume_already_available"
1461
- : askResult.request_pending
1462
- ? "resume_request_already_pending"
1463
- : askResult.ok
1464
- ? "already_requested"
1465
- : (askResult.error || "ask_resume_not_clicked")
1466
- };
1467
- if (askResult.attachment_resume_available) {
1468
- attempts.push({
1469
- attempt: attempt + 1,
1470
- ask_result: askResult,
1471
- confirm_result: confirmResult,
1472
- message_before_count: before.count,
1473
- message_after_count: before.count,
1474
- message_observed: false,
1475
- message_last_text: before.last_text || ""
1476
- });
1477
- return {
1478
- requested: false,
1479
- skipped: true,
1480
- reason: "attachment_resume_already_available",
1481
- initial_state: initialState,
1482
- close_before_greeting: closeBeforeGreeting,
1483
- greeting_sent: true,
1484
- greeting_send_result: sendResult,
1485
- attempts
1486
- };
1487
- }
1488
- if (askResult.request_pending || askResult.already_requested) {
1489
- attempts.push({
1490
- attempt: attempt + 1,
1491
- ask_result: askResult,
1492
- confirm_result: confirmResult,
1493
- message_before_count: before.count,
1494
- message_after_count: before.count,
1495
- resume_attachment_before_count: before.resume_attachment_count || 0,
1496
- resume_attachment_after_count: before.resume_attachment_count || 0,
1497
- message_observed: false,
1498
- message_last_text: before.last_success_text || before.last_text || ""
1499
- });
1500
- return {
1501
- requested: false,
1502
- skipped: true,
1503
- reason: "resume_request_already_pending",
1504
- initial_state: initialState,
1505
- close_before_greeting: closeBeforeGreeting,
1506
- greeting_sent: true,
1507
- greeting_send_result: sendResult,
1508
- attempts
1509
- };
1510
- }
1511
- if (askResult.ok && !askResult.already_requested) {
1512
- confirmResult = await clickChatConfirmRequestResume(client);
1513
- }
1514
- const messageCheck = await waitForChatResumeRequestMessage(client, {
1515
- baselineCount: before.count,
1516
- baselineResumeAttachmentCount: before.resume_attachment_count
1517
- });
1518
- const messageObserved = Boolean(messageCheck.observed);
1519
- attempts.push({
1520
- attempt: attempt + 1,
1521
- ask_result: askResult,
1522
- confirm_result: confirmResult,
1523
- message_before_count: before.count,
1524
- message_after_count: messageCheck.state?.count || 0,
1525
- resume_attachment_before_count: before.resume_attachment_count || 0,
1526
- resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
1527
- message_observed: messageObserved,
1528
- message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || ""
1529
- });
1530
- if (messageObserved) {
1531
- return {
1532
- requested: true,
1533
- skipped: false,
1534
- reason: "requested",
1535
- initial_state: initialState,
1536
- close_before_greeting: closeBeforeGreeting,
1537
- greeting_sent: true,
1538
- greeting_send_result: sendResult,
1539
- attempts
1540
- };
1541
- }
1542
- await sleep(900);
1543
- }
1544
-
1545
- return {
1546
- requested: false,
1547
- skipped: false,
1548
- reason: "resume_request_message_not_observed",
1549
- initial_state: initialState,
1550
- close_before_greeting: closeBeforeGreeting,
1551
- greeting_sent: true,
1552
- greeting_send_result: sendResult,
1553
- attempts
1554
- };
1555
- }
2618
+ let greetingBaseline = null;
2619
+ let editorState = null;
2620
+ let sendResult = null;
2621
+ if (!skipGreeting) {
2622
+ greetingBaseline = await getChatGreetingMessageState(client, effectiveGreetingText);
2623
+ editorState = await setChatEditorMessage(client, effectiveGreetingText);
2624
+ if (!editorState.ok) {
2625
+ const error = new Error(editorState.error || "CHAT_EDITOR_MESSAGE_MISMATCH");
2626
+ error.code = editorState.error || "CHAT_EDITOR_MESSAGE_MISMATCH";
2627
+ error.retryable = true;
2628
+ error.attempts = Array.isArray(editorState.attempts)
2629
+ ? editorState.attempts
2630
+ : [];
2631
+ throw error;
2632
+ }
2633
+ if (typeof actionTransition === "function") {
2634
+ await actionTransition("greeting_send_in_flight", {
2635
+ greeting_baseline_count: greetingBaseline.exact_count,
2636
+ greeting_evidence_readable: greetingBaseline.ok
2637
+ });
2638
+ }
2639
+ sendResult = await sendChatMessage(client, effectiveGreetingText);
2640
+ if (!sendResult.sent) {
2641
+ if (typeof actionTransition === "function") {
2642
+ await actionTransition("outcome_unknown", {
2643
+ action: "greeting_send",
2644
+ reason: sendResult.error || sendResult.method || "unknown"
2645
+ });
2646
+ }
2647
+ throw new Error(`CHAT_GREETING_SEND_FAILED:${sendResult.error || sendResult.method || "unknown"}`);
2648
+ }
2649
+ const postGreetingPageState = await getChatTopLevelState(client);
2650
+ if (postGreetingPageState.is_security_verification) {
2651
+ if (typeof actionTransition === "function") {
2652
+ await actionTransition("outcome_unknown", {
2653
+ action: "greeting_send",
2654
+ reason: "boss_security_verification_required"
2655
+ });
2656
+ }
2657
+ throw makeBossSecurityVerificationRequiredError(
2658
+ postGreetingPageState,
2659
+ "chat_greeting_confirmation"
2660
+ );
2661
+ }
2662
+ if (typeof actionTransition === "function") {
2663
+ await actionTransition("greeting_confirmed", {
2664
+ greeting_baseline_count: greetingBaseline.exact_count,
2665
+ send_method: sendResult.method || null
2666
+ });
2667
+ }
2668
+ }
2669
+
2670
+ const attempts = [];
2671
+ const before = await getChatResumeRequestMessageState(client);
2672
+ if (typeof actionTransition === "function") {
2673
+ await actionTransition("request_in_flight", {
2674
+ request_baseline_count: before.count,
2675
+ resume_attachment_baseline_count: before.resume_attachment_count || 0
2676
+ });
2677
+ }
2678
+ const askResult = await clickChatAskResume(client, {
2679
+ timeoutMs: askResumeTimeoutMs,
2680
+ skipWhenAttachmentResumeAvailable
2681
+ });
2682
+ let confirmResult = {
2683
+ confirmed: false,
2684
+ assumed_requested: Boolean(askResult.already_requested),
2685
+ skipped: true,
2686
+ reason: askResult.attachment_resume_available
2687
+ ? "attachment_resume_already_available"
2688
+ : askResult.request_pending
2689
+ ? "resume_request_already_pending"
2690
+ : askResult.ok
2691
+ ? "already_requested"
2692
+ : (askResult.error || "ask_resume_not_clicked")
2693
+ };
2694
+ if (askResult.ok && !askResult.already_requested) {
2695
+ confirmResult = await clickChatConfirmRequestResume(client, {
2696
+ timeoutMs: confirmResumeTimeoutMs,
2697
+ pollIntervalMs: confirmResumePollIntervalMs
2698
+ });
2699
+ }
2700
+ let messageCheck = (askResult.attachment_resume_available || askResult.request_pending || askResult.already_requested)
2701
+ ? { observed: false, state: before, elapsed_ms: 0 }
2702
+ : await waitForChatResumeRequestMessage(client, {
2703
+ baselineCount: before.count,
2704
+ baselineResumeAttachmentCount: before.resume_attachment_count,
2705
+ timeoutMs: resolveChatRequestVerificationTimeoutMs(
2706
+ requestVerificationTimeoutMs,
2707
+ maxAttempts
2708
+ )
2709
+ });
2710
+ let finalReadyState = confirmResult?.assumed_requested
2711
+ ? (confirmResult.state || null)
2712
+ : null;
2713
+ let passiveVerificationAttempted = false;
2714
+ if (!messageCheck.observed && !finalReadyState) {
2715
+ passiveVerificationAttempted = true;
2716
+ const settleMs = Math.max(0, Number(requestFinalVerificationSettleMs) || 0);
2717
+ if (settleMs > 0) await sleep(settleMs);
2718
+ const {
2719
+ message_state: freshMessageState,
2720
+ ready_state: freshReadyState
2721
+ } = await readChatRequestVerificationEvidence(client);
2722
+ messageCheck = {
2723
+ observed: Boolean(
2724
+ freshMessageState.count > before.count
2725
+ || freshMessageState.resume_attachment_count > before.resume_attachment_count
2726
+ ),
2727
+ elapsed_ms: messageCheck.elapsed_ms + settleMs,
2728
+ state: freshMessageState,
2729
+ passive_final_read: true
2730
+ };
2731
+ finalReadyState = freshReadyState;
2732
+ }
2733
+ const postRequestPageState = await getChatTopLevelState(client);
2734
+ if (postRequestPageState.is_security_verification) {
2735
+ if (typeof actionTransition === "function") {
2736
+ await actionTransition("outcome_unknown", {
2737
+ action: "request_resume",
2738
+ reason: "boss_security_verification_required"
2739
+ });
2740
+ }
2741
+ throw makeBossSecurityVerificationRequiredError(
2742
+ postRequestPageState,
2743
+ "chat_resume_request_confirmation"
2744
+ );
2745
+ }
2746
+ const messageObserved = Boolean(
2747
+ (messageCheck.state?.count || 0) > (before.count || 0)
2748
+ );
2749
+ const attachmentObserved = Boolean(
2750
+ (messageCheck.state?.resume_attachment_count || 0) > (before.resume_attachment_count || 0)
2751
+ );
2752
+ const readyStateObserved = Boolean(
2753
+ finalReadyState?.already_requested_resume
2754
+ || finalReadyState?.attachment_resume_enabled
2755
+ );
2756
+ const requestObserved = messageObserved || attachmentObserved || readyStateObserved;
2757
+ const confirmationSource = messageObserved
2758
+ ? "message_delta"
2759
+ : attachmentObserved
2760
+ ? "attachment_resume_delta"
2761
+ : finalReadyState?.already_requested_resume
2762
+ ? "exact_requested_resume_state"
2763
+ : finalReadyState?.attachment_resume_enabled
2764
+ ? "attachment_resume_available"
2765
+ : null;
2766
+ attempts.push({
2767
+ attempt: 1,
2768
+ ask_result: askResult,
2769
+ confirm_result: confirmResult,
2770
+ message_before_count: before.count,
2771
+ message_after_count: messageCheck.state?.count || 0,
2772
+ resume_attachment_before_count: before.resume_attachment_count || 0,
2773
+ resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
2774
+ message_observed: messageObserved,
2775
+ message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || "",
2776
+ ready_state_observed: readyStateObserved,
2777
+ confirmation_source: confirmationSource,
2778
+ passive_verification_attempted: passiveVerificationAttempted
2779
+ });
2780
+
2781
+ const satisfiedReason = askResult.attachment_resume_available
2782
+ ? "attachment_resume_already_available"
2783
+ : (askResult.request_pending || askResult.already_requested)
2784
+ ? "resume_request_already_pending"
2785
+ : requestObserved
2786
+ ? "requested"
2787
+ : "";
2788
+ if (satisfiedReason) {
2789
+ if (typeof actionTransition === "function") {
2790
+ await actionTransition("request_confirmed", {
2791
+ reason: satisfiedReason,
2792
+ message_observed: messageObserved,
2793
+ request_ready_state_observed: readyStateObserved,
2794
+ request_confirmation_source: confirmationSource,
2795
+ request_after_count: messageCheck.state?.count || before.count,
2796
+ resume_attachment_after_count: messageCheck.state?.resume_attachment_count || before.resume_attachment_count || 0
2797
+ });
2798
+ }
2799
+ return {
2800
+ requested: satisfiedReason === "requested",
2801
+ skipped: satisfiedReason !== "requested",
2802
+ reason: satisfiedReason,
2803
+ initial_state: initialState,
2804
+ close_before_greeting: closeBeforeGreeting,
2805
+ greeting_sent: !skipGreeting,
2806
+ greeting_skipped_from_journal: Boolean(skipGreeting),
2807
+ greeting_send_result: sendResult,
2808
+ attempts
2809
+ };
2810
+ }
2811
+
2812
+ if (typeof actionTransition === "function") {
2813
+ await actionTransition("outcome_unknown", {
2814
+ action: "request_resume",
2815
+ reason: "resume_request_message_not_observed",
2816
+ ask_result: {
2817
+ ok: Boolean(askResult.ok),
2818
+ error: askResult.error || null
2819
+ },
2820
+ confirm_result: {
2821
+ confirmed: Boolean(confirmResult.confirmed),
2822
+ error: confirmResult.error || null
2823
+ }
2824
+ });
2825
+ }
2826
+ return {
2827
+ requested: false,
2828
+ skipped: false,
2829
+ reason: "resume_request_message_not_observed",
2830
+ outcome_unknown: true,
2831
+ initial_state: initialState,
2832
+ close_before_greeting: closeBeforeGreeting,
2833
+ greeting_sent: !skipGreeting,
2834
+ greeting_skipped_from_journal: Boolean(skipGreeting),
2835
+ greeting_send_result: sendResult,
2836
+ attempts
2837
+ };
2838
+ }
1556
2839
 
1557
2840
  export async function closeChatResumeModal(client, {
1558
2841
  attemptsLimit = 3