@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
3
  import { createRunLifecycleManager } from "../../core/run/index.js";
4
4
  import {
5
5
  addTiming,
@@ -88,9 +88,304 @@ import {
88
88
  resolveRecommendPostAction,
89
89
  waitForRecommendDetailActionControls
90
90
  } from "./actions.js";
91
- import { getRecommendRoots } from "./roots.js";
92
-
93
- function normalizeLabels(labels = []) {
91
+ import { getRecommendRoots } from "./roots.js";
92
+
93
+ const RECOMMEND_DEBUG_BOUNDARY_MODES = Object.freeze({
94
+ list_end: "debug_force_list_end_after_processed",
95
+ context_recovery: "debug_force_context_recovery_after_processed",
96
+ cdp_reconnect: "debug_force_cdp_reconnect_after_processed"
97
+ });
98
+
99
+ function hasOwn(source, key) {
100
+ return Boolean(source && Object.prototype.hasOwnProperty.call(source, key));
101
+ }
102
+
103
+ function readDebugBoundaryValue(source, snakeKey, camelKey) {
104
+ if (hasOwn(source, snakeKey)) return source[snakeKey];
105
+ if (hasOwn(source, camelKey)) return source[camelKey];
106
+ return null;
107
+ }
108
+
109
+ function normalizeDebugBoundaryThreshold(raw, field) {
110
+ if (raw === undefined || raw === null || raw === "") return null;
111
+ const parsed = Number(raw);
112
+ if (!Number.isInteger(parsed) || parsed <= 0) {
113
+ const error = new Error(`${field} must be a positive integer`);
114
+ error.code = "INVALID_RECOMMEND_DEBUG_BOUNDARY";
115
+ throw error;
116
+ }
117
+ return parsed;
118
+ }
119
+
120
+ export function normalizeRecommendDebugBoundaryOptions(source = {}) {
121
+ const debugTestMode = source.debugTestMode === true || source.debug_test_mode === true;
122
+ const thresholds = {
123
+ list_end: normalizeDebugBoundaryThreshold(
124
+ readDebugBoundaryValue(
125
+ source,
126
+ RECOMMEND_DEBUG_BOUNDARY_MODES.list_end,
127
+ "debugForceListEndAfterProcessed"
128
+ ),
129
+ RECOMMEND_DEBUG_BOUNDARY_MODES.list_end
130
+ ),
131
+ context_recovery: normalizeDebugBoundaryThreshold(
132
+ readDebugBoundaryValue(
133
+ source,
134
+ RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery,
135
+ "debugForceContextRecoveryAfterProcessed"
136
+ ),
137
+ RECOMMEND_DEBUG_BOUNDARY_MODES.context_recovery
138
+ ),
139
+ cdp_reconnect: normalizeDebugBoundaryThreshold(
140
+ readDebugBoundaryValue(
141
+ source,
142
+ RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect,
143
+ "debugForceCdpReconnectAfterProcessed"
144
+ ),
145
+ RECOMMEND_DEBUG_BOUNDARY_MODES.cdp_reconnect
146
+ )
147
+ };
148
+ const configured = Object.entries(thresholds).filter(([, threshold]) => threshold !== null);
149
+ if (configured.length > 1) {
150
+ const error = new Error(
151
+ `${configured.map(([mode]) => RECOMMEND_DEBUG_BOUNDARY_MODES[mode]).join(", ")} are mutually exclusive`
152
+ );
153
+ error.code = "RECOMMEND_DEBUG_BOUNDARIES_MUTUALLY_EXCLUSIVE";
154
+ throw error;
155
+ }
156
+ if (configured.length && !debugTestMode) {
157
+ const error = new Error(
158
+ `${RECOMMEND_DEBUG_BOUNDARY_MODES[configured[0][0]]} requires debug_test_mode=true`
159
+ );
160
+ error.code = "DEBUG_TEST_MODE_REQUIRED";
161
+ throw error;
162
+ }
163
+ const [configuredEntry = null] = configured;
164
+ return {
165
+ debugTestMode,
166
+ mode: configuredEntry?.[0] || null,
167
+ field: configuredEntry ? RECOMMEND_DEBUG_BOUNDARY_MODES[configuredEntry[0]] : null,
168
+ threshold: configuredEntry?.[1] ?? null,
169
+ debugForceListEndAfterProcessed: thresholds.list_end,
170
+ debugForceContextRecoveryAfterProcessed: thresholds.context_recovery,
171
+ debugForceCdpReconnectAfterProcessed: thresholds.cdp_reconnect
172
+ };
173
+ }
174
+
175
+ export function createRecommendDebugBoundaryController(source = {}) {
176
+ const config = normalizeRecommendDebugBoundaryOptions(source);
177
+ let triggered = false;
178
+ let triggerCount = 0;
179
+ return {
180
+ config,
181
+ take(processedCount) {
182
+ const processed = Math.max(0, Number(processedCount) || 0);
183
+ if (triggered || !config.mode || processed < config.threshold) return null;
184
+ triggered = true;
185
+ triggerCount += 1;
186
+ return {
187
+ mode: config.mode,
188
+ field: config.field,
189
+ threshold: config.threshold,
190
+ processed,
191
+ trigger_count: triggerCount
192
+ };
193
+ },
194
+ getState() {
195
+ return {
196
+ ...config,
197
+ triggered,
198
+ trigger_count: triggerCount
199
+ };
200
+ }
201
+ };
202
+ }
203
+
204
+ function compactListReadStaleDiagnostic(error, {
205
+ attempt = 0,
206
+ exhausted = false
207
+ } = {}) {
208
+ return {
209
+ code: error?.code || "RECOMMEND_LIST_READ_STALE_NODE",
210
+ message: String(error?.message || error || "Stale recommend list node").slice(0, 500),
211
+ phase: "recommend:list-read",
212
+ cdp_method: error?.cdp_method || null,
213
+ cdp_at: error?.cdp_at ? String(error.cdp_at).slice(0, 100) : null,
214
+ cdp_node_id: Number.isInteger(error?.cdp_node_id) ? error.cdp_node_id : null,
215
+ cdp_backend_node_id: Number.isInteger(error?.cdp_backend_node_id)
216
+ ? error.cdp_backend_node_id
217
+ : null,
218
+ cdp_param_keys: Array.isArray(error?.cdp_param_keys)
219
+ ? error.cdp_param_keys
220
+ .map((key) => String(key || "").trim())
221
+ .filter((key) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key))
222
+ .slice(0, 20)
223
+ : [],
224
+ attempt,
225
+ exhausted: Boolean(exhausted),
226
+ at: new Date().toISOString()
227
+ };
228
+ }
229
+
230
+ function annotateListReadStaleFailure(error, diagnostics, {
231
+ exhausted = false,
232
+ recoveryFailed = false
233
+ } = {}) {
234
+ if (!error || typeof error !== "object") return error;
235
+ error.phase = error.phase || "recommend:list-read";
236
+ error.list_read_stale_recovery_attempts = diagnostics;
237
+ if (exhausted) error.list_read_stale_recovery_exhausted = true;
238
+ if (recoveryFailed) error.list_read_stale_recovery_failed = true;
239
+ return error;
240
+ }
241
+
242
+ export async function acquireRecommendListReadWithStaleRecovery({
243
+ acquire,
244
+ recover,
245
+ maxRetries = 2,
246
+ onStale = null,
247
+ onRecoveryApplied = null,
248
+ onRecovered = null,
249
+ onExhausted = null
250
+ } = {}) {
251
+ if (typeof acquire !== "function") {
252
+ throw new Error("acquireRecommendListReadWithStaleRecovery requires acquire");
253
+ }
254
+ if (typeof recover !== "function") {
255
+ throw new Error("acquireRecommendListReadWithStaleRecovery requires recover");
256
+ }
257
+ const retryLimit = Math.max(0, Number.isInteger(maxRetries) ? maxRetries : 2);
258
+ const diagnostics = [];
259
+ let acquireAttempt = 0;
260
+ let pendingRecoveryDiagnostic = null;
261
+ while (true) {
262
+ acquireAttempt += 1;
263
+ try {
264
+ const result = await acquire({
265
+ acquireAttempt,
266
+ recoveryCount: diagnostics.filter((item) => item.recovered === true).length
267
+ });
268
+ if (pendingRecoveryDiagnostic) {
269
+ pendingRecoveryDiagnostic.recovered = true;
270
+ pendingRecoveryDiagnostic.recovered_at = new Date().toISOString();
271
+ if (typeof onRecovered === "function") {
272
+ await onRecovered({
273
+ diagnostic: pendingRecoveryDiagnostic,
274
+ diagnostics: diagnostics.slice()
275
+ });
276
+ }
277
+ pendingRecoveryDiagnostic = null;
278
+ }
279
+ return {
280
+ result,
281
+ acquire_attempts: acquireAttempt,
282
+ stale_diagnostics: diagnostics
283
+ };
284
+ } catch (error) {
285
+ if (!isStaleRecommendNodeError(error)) throw error;
286
+ pendingRecoveryDiagnostic = null;
287
+ const staleAttempt = diagnostics.length + 1;
288
+ const exhausted = staleAttempt > retryLimit;
289
+ const diagnostic = compactListReadStaleDiagnostic(error, {
290
+ attempt: staleAttempt,
291
+ exhausted
292
+ });
293
+ diagnostics.push(diagnostic);
294
+ if (exhausted) {
295
+ if (typeof onExhausted === "function") {
296
+ await onExhausted({ error, diagnostic, diagnostics: diagnostics.slice() });
297
+ }
298
+ throw annotateListReadStaleFailure(error, diagnostics, { exhausted: true });
299
+ }
300
+ if (typeof onStale === "function") {
301
+ await onStale({ error, diagnostic, diagnostics: diagnostics.slice() });
302
+ }
303
+ let recoveryResult = null;
304
+ try {
305
+ recoveryResult = await recover({ error, diagnostic, diagnostics: diagnostics.slice() });
306
+ } catch (recoveryError) {
307
+ if (recoveryError && typeof recoveryError === "object" && !recoveryError.cause) {
308
+ recoveryError.cause = error;
309
+ }
310
+ throw annotateListReadStaleFailure(recoveryError, diagnostics, {
311
+ recoveryFailed: true
312
+ });
313
+ }
314
+ diagnostic.recovery_applied = true;
315
+ diagnostic.recovery_mode = recoveryResult?.recovery_mode || "unknown";
316
+ diagnostic.recovery_applied_at = new Date().toISOString();
317
+ pendingRecoveryDiagnostic = diagnostic;
318
+ if (typeof onRecoveryApplied === "function") {
319
+ await onRecoveryApplied({
320
+ error,
321
+ diagnostic,
322
+ recoveryResult,
323
+ diagnostics: diagnostics.slice()
324
+ });
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ export async function recoverRecommendListReadStaleContext({
331
+ staleAttempt = 1,
332
+ listState,
333
+ rootReacquire,
334
+ contextReapply
335
+ } = {}) {
336
+ if (typeof rootReacquire !== "function") {
337
+ throw new Error("recoverRecommendListReadStaleContext requires rootReacquire");
338
+ }
339
+ if (typeof contextReapply !== "function") {
340
+ throw new Error("recoverRecommendListReadStaleContext requires contextReapply");
341
+ }
342
+ const processedKeys = new Set(listState?.processed_keys || []);
343
+ try {
344
+ if (Number(staleAttempt) <= 1) {
345
+ try {
346
+ const rootResult = await rootReacquire();
347
+ if (Number(rootResult?.card_count) <= 0) {
348
+ const noCardsError = new Error("Recommend list root reacquire returned no candidate cards");
349
+ noCardsError.code = "RECOMMEND_LIST_ROOT_REACQUIRE_NO_CARDS";
350
+ throw noCardsError;
351
+ }
352
+ resetInfiniteListForRefreshRound(listState, {
353
+ reason: "list_read_stale_node_root_reacquire",
354
+ round: 1,
355
+ method: "root_reacquire",
356
+ metadata: {
357
+ card_count: Number(rootResult?.card_count) || 0,
358
+ processed_count: processedKeys.size
359
+ }
360
+ });
361
+ return {
362
+ recovery_mode: "root_reacquire",
363
+ root_reacquire: rootResult || null
364
+ };
365
+ } catch (rootReacquireError) {
366
+ const contextResult = await contextReapply({ rootReacquireError });
367
+ return {
368
+ recovery_mode: "context_reapply",
369
+ escalated_from: "root_reacquire",
370
+ root_reacquire_error: compactListReadStaleDiagnostic(rootReacquireError, {
371
+ attempt: 1
372
+ }),
373
+ context_reapply: contextResult || null
374
+ };
375
+ }
376
+ }
377
+ const contextResult = await contextReapply({ rootReacquireError: null });
378
+ return {
379
+ recovery_mode: "context_reapply",
380
+ escalated_from: "repeated_stale",
381
+ context_reapply: contextResult || null
382
+ };
383
+ } finally {
384
+ for (const key of processedKeys) listState?.processed_keys?.add(key);
385
+ }
386
+ }
387
+
388
+ function normalizeLabels(labels = []) {
94
389
  return labels.map((label) => String(label || "").trim()).filter(Boolean);
95
390
  }
96
391
 
@@ -451,9 +746,10 @@ function compactRefreshAttempt(refreshAttempt) {
451
746
  if (!refreshAttempt) return null;
452
747
  return {
453
748
  ok: Boolean(refreshAttempt.ok),
454
- method: refreshAttempt.method || "",
455
- reason: refreshAttempt.reason || null,
456
- error: refreshAttempt.error || null,
749
+ method: refreshAttempt.method || "",
750
+ reason: refreshAttempt.reason || null,
751
+ error: refreshAttempt.error || null,
752
+ error_diagnostic: refreshAttempt.error_diagnostic || null,
457
753
  forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
458
754
  target_url: refreshAttempt.target_url || null,
459
755
  card_count: refreshAttempt.card_count || 0,
@@ -468,9 +764,10 @@ function compactRefreshAttempt(refreshAttempt) {
468
764
  : null,
469
765
  attempts: (refreshAttempt.attempts || []).map((attempt) => ({
470
766
  ok: Boolean(attempt.ok),
471
- method: attempt.method || "",
472
- reason: attempt.reason || null,
767
+ method: attempt.method || "",
768
+ reason: attempt.reason || null,
473
769
  error: attempt.error || null,
770
+ error_diagnostic: attempt.error_diagnostic || null,
474
771
  label: attempt.label || null,
475
772
  before_card_count: attempt.before_card_count || 0,
476
773
  after_card_count: attempt.after_card_count || 0,
@@ -482,6 +779,7 @@ function compactRefreshAttempt(refreshAttempt) {
482
779
  method: cityAttempt.method || "current_city_only_reapply",
483
780
  reason: cityAttempt.reason || null,
484
781
  error: cityAttempt.error || null,
782
+ error_diagnostic: cityAttempt.error_diagnostic || null,
485
783
  attempt: cityAttempt.attempt || 0,
486
784
  result: compactCurrentCityOnlyResult(cityAttempt.result)
487
785
  })),
@@ -492,6 +790,7 @@ function compactRefreshAttempt(refreshAttempt) {
492
790
  method: attempt.method || "current_city_only_reapply",
493
791
  reason: attempt.reason || null,
494
792
  error: attempt.error || null,
793
+ error_diagnostic: attempt.error_diagnostic || null,
495
794
  attempt: attempt.attempt || 0,
496
795
  result: compactCurrentCityOnlyResult(attempt.result)
497
796
  })),
@@ -499,14 +798,16 @@ function compactRefreshAttempt(refreshAttempt) {
499
798
  ok: Boolean(attempt.ok),
500
799
  method: attempt.method || "filter_reapply",
501
800
  reason: attempt.reason || null,
502
- error: attempt.error || null,
801
+ error: attempt.error || null,
802
+ error_diagnostic: attempt.error_diagnostic || null,
503
803
  attempt: attempt.attempt || 0
504
804
  })),
505
805
  job_selection_attempts: (refreshAttempt.job_selection_attempts || []).map((attempt) => ({
506
806
  ok: Boolean(attempt.ok),
507
- method: attempt.method || "job_select",
508
- reason: attempt.reason || null,
509
- error: attempt.error || null,
807
+ method: attempt.method || "job_select",
808
+ reason: attempt.reason || null,
809
+ error: attempt.error || null,
810
+ error_diagnostic: attempt.error_diagnostic || null,
510
811
  attempt: attempt.attempt || 0,
511
812
  iframe_document_node_id: attempt.iframe_document_node_id || 0,
512
813
  selected: Boolean(attempt.selected),
@@ -585,9 +886,12 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
585
886
  if (error.phase) {
586
887
  result.phase = error.phase;
587
888
  }
588
- if (error.refresh_attempt) {
589
- result.refresh_attempt = error.refresh_attempt;
590
- }
889
+ if (error.refresh_attempt) {
890
+ result.refresh_attempt = error.refresh_attempt;
891
+ }
892
+ if (error.error_diagnostic) {
893
+ result.error_diagnostic = error.error_diagnostic;
894
+ }
591
895
  if (error.list_end_reason) {
592
896
  result.list_end_reason = error.list_end_reason;
593
897
  }
@@ -628,7 +932,45 @@ function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "")
628
932
  return error;
629
933
  }
630
934
 
631
- function createRecommendRefreshFailureError(refreshAttempt, {
935
+ function findRecommendRefreshErrorDiagnostic(refreshAttempt) {
936
+ if (!refreshAttempt || typeof refreshAttempt !== "object") return null;
937
+ if (refreshAttempt.error_diagnostic) return refreshAttempt.error_diagnostic;
938
+ const candidates = [
939
+ ...(refreshAttempt.attempts || []),
940
+ ...(refreshAttempt.current_city_only_attempts || []),
941
+ ...(refreshAttempt.filter_reapply_attempts || []),
942
+ ...(refreshAttempt.job_selection_attempts || [])
943
+ ].reverse();
944
+ for (const attempt of candidates) {
945
+ if (attempt?.error_diagnostic) return attempt.error_diagnostic;
946
+ const nestedCityAttempts = (attempt?.current_city_only_attempts || []).slice().reverse();
947
+ for (const cityAttempt of nestedCityAttempts) {
948
+ if (cityAttempt?.error_diagnostic) return cityAttempt.error_diagnostic;
949
+ }
950
+ }
951
+ return null;
952
+ }
953
+
954
+ function attachRecommendRefreshErrorDiagnostic(error, refreshAttempt) {
955
+ const diagnostic = findRecommendRefreshErrorDiagnostic(refreshAttempt);
956
+ if (!error || !diagnostic) return error;
957
+ error.error_diagnostic = diagnostic;
958
+ for (const key of [
959
+ "cdp_method",
960
+ "cdp_at",
961
+ "cdp_node_id",
962
+ "cdp_backend_node_id",
963
+ "cdp_search_id",
964
+ "cdp_param_keys"
965
+ ]) {
966
+ if (diagnostic[key] !== undefined && error[key] === undefined) {
967
+ error[key] = diagnostic[key];
968
+ }
969
+ }
970
+ return error;
971
+ }
972
+
973
+ export function createRecommendRefreshFailureError(refreshAttempt, {
632
974
  listEndReason = "",
633
975
  targetCount = 0,
634
976
  passedCount = 0
@@ -639,10 +981,10 @@ function createRecommendRefreshFailureError(refreshAttempt, {
639
981
  error.code = "RECOMMEND_END_REFRESH_FAILED";
640
982
  error.refresh_attempt = refreshAttempt || null;
641
983
  error.list_end_reason = listEndReason || null;
642
- error.target_count = targetCount;
643
- error.passed_count = passedCount;
644
- return error;
645
- }
984
+ error.target_count = targetCount;
985
+ error.passed_count = passedCount;
986
+ return attachRecommendRefreshErrorDiagnostic(error, refreshAttempt);
987
+ }
646
988
 
647
989
  export function isRecoverableImageCaptureError(error) {
648
990
  const code = String(error?.code || "");
@@ -778,12 +1120,22 @@ export async function runRecommendWorkflow({
778
1120
  llmImageLimit = 8,
779
1121
  llmImageDetail = "high",
780
1122
  imageOutputDir = "",
781
- humanRestEnabled = false,
782
- humanBehavior = null,
783
- skipRecentColleagueContacted = true,
784
- colleagueContactWindowDays = 14
785
- } = {}, runControl) {
786
- if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
1123
+ humanRestEnabled = false,
1124
+ humanBehavior = null,
1125
+ skipRecentColleagueContacted = true,
1126
+ colleagueContactWindowDays = 14,
1127
+ debugTestMode = false,
1128
+ debugForceListEndAfterProcessed = null,
1129
+ debugForceContextRecoveryAfterProcessed = null,
1130
+ debugForceCdpReconnectAfterProcessed = null
1131
+ } = {}, runControl) {
1132
+ if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
1133
+ const debugBoundary = createRecommendDebugBoundaryController({
1134
+ debugTestMode,
1135
+ debugForceListEndAfterProcessed,
1136
+ debugForceContextRecoveryAfterProcessed,
1137
+ debugForceCdpReconnectAfterProcessed
1138
+ });
787
1139
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
788
1140
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
789
1141
  });
@@ -848,6 +1200,13 @@ export async function runRecommendWorkflow({
848
1200
  let cardNodeIds = [];
849
1201
  let listEndReason = "";
850
1202
  let lastHumanEvent = null;
1203
+ let debugForceReconnectPending = null;
1204
+ let listReadStaleRecoveryAttempts = 0;
1205
+ let listReadStaleRecoveryApplied = 0;
1206
+ let listReadStaleRecoveries = 0;
1207
+ let lastListReadStaleDiagnostic = null;
1208
+ let lastListReadRecoveryMode = null;
1209
+ const listReadStaleDiagnostics = [];
851
1210
  const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
852
1211
  rootNodeId: rootState?.iframe?.documentNodeId,
853
1212
  containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
@@ -883,15 +1242,16 @@ export async function runRecommendWorkflow({
883
1242
  });
884
1243
  }
885
1244
 
886
- function updateRecommendProgress(extra = {}) {
887
- const counts = countRecommendResultStatuses(results, { greetCount });
1245
+ function updateRecommendProgress(extra = {}) {
1246
+ const counts = countRecommendResultStatuses(results, { greetCount });
888
1247
  const listSnapshot = compactInfiniteListState(listState);
889
1248
  const humanRestState = humanRestController.getState();
890
1249
  runControl.updateProgress({
891
1250
  card_count: cardNodeIds.length,
892
- target_count: targetPassCount,
893
- target_count_semantics: "passed_candidates",
894
- ...counts,
1251
+ target_count: targetPassCount,
1252
+ target_count_semantics: "passed_candidates",
1253
+ ...counts,
1254
+ transient_recovered: counts.transient_recovered + listReadStaleRecoveries,
895
1255
  screening_mode: normalizedScreeningMode,
896
1256
  unique_seen: listSnapshot.seen_count,
897
1257
  scroll_count: listSnapshot.scroll_count,
@@ -913,9 +1273,18 @@ export async function runRecommendWorkflow({
913
1273
  human_rest_count: humanRestState.rest_count,
914
1274
  human_rest_ms: humanRestState.total_rest_ms,
915
1275
  last_human_event: lastHumanEvent,
1276
+ debug_boundary_mode: debugBoundary.config.mode,
1277
+ debug_boundary_threshold: debugBoundary.config.threshold,
1278
+ debug_boundary_triggered: debugBoundary.getState().triggered,
1279
+ debug_boundary_trigger_count: debugBoundary.getState().trigger_count,
1280
+ list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
1281
+ list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
1282
+ list_read_stale_recoveries: listReadStaleRecoveries,
1283
+ last_list_read_recovery_mode: lastListReadRecoveryMode,
1284
+ last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
916
1285
  ...extra
917
1286
  });
918
- }
1287
+ }
919
1288
 
920
1289
  function checkpointInProgressCandidate({
921
1290
  index = results.length,
@@ -1003,7 +1372,13 @@ export async function runRecommendWorkflow({
1003
1372
  refresh_forced_recent_not_view: effectiveForceRecentNotView,
1004
1373
  recovery_reason: reason
1005
1374
  });
1006
- throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
1375
+ const recoveryError = new Error(
1376
+ `Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`
1377
+ );
1378
+ recoveryError.code = "RECOMMEND_CONTEXT_RECOVERY_FAILED";
1379
+ recoveryError.refresh_attempt = compactRefresh;
1380
+ recoveryError.recovery_reason = reason;
1381
+ throw attachRecommendRefreshErrorDiagnostic(recoveryError, compactRefresh);
1007
1382
  }
1008
1383
  if (refreshResult.current_city_only) {
1009
1384
  currentCityOnlyResult = refreshResult.current_city_only;
@@ -1149,50 +1524,247 @@ export async function runRecommendWorkflow({
1149
1524
  list_end_reason: null
1150
1525
  });
1151
1526
 
1152
- while (countPassedResults(results) < targetPassCount) {
1153
- const candidateStarted = Date.now();
1154
- const timings = {};
1155
- await runControl.waitIfPaused();
1156
- runControl.throwIfCanceled();
1157
- runControl.setPhase("recommend:candidate");
1158
- rootState = await ensureRecommendViewport(rootState, "candidate_loop");
1159
-
1160
- const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1161
- client,
1162
- state: listState,
1163
- maxScrolls: listMaxScrolls,
1164
- stableSignatureLimit: listStableSignatureLimit,
1165
- wheelDeltaY: listWheelDeltaY,
1166
- settleMs: listSettleMs,
1167
- listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1168
- fallbackPoint: listFallbackResolver,
1169
- findNodeIds: async () => {
1170
- let currentRootState = await getRecommendRoots(client);
1171
- currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
1172
- rootState = currentRootState;
1173
- const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
1174
- timeoutMs: Math.min(cardTimeoutMs, 5000),
1175
- intervalMs: 300
1176
- });
1177
- cardNodeIds = currentCardNodeIds;
1178
- return currentCardNodeIds;
1179
- },
1180
- readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
1181
- targetUrl,
1182
- source: "recommend-run-card",
1183
- metadata: {
1184
- run_candidate_index: results.length,
1185
- visible_index: visibleIndex
1186
- }
1187
- }),
1188
- detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1189
- rootNodeId: rootState?.iframe?.documentNodeId,
1190
- markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
1191
- refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
1192
- textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1193
- maxTextScanNodes: 500
1194
- })
1195
- }));
1527
+ while (countPassedResults(results) < targetPassCount) {
1528
+ const candidateStarted = Date.now();
1529
+ const timings = {};
1530
+ await runControl.waitIfPaused();
1531
+ runControl.throwIfCanceled();
1532
+ runControl.setPhase("recommend:list-read");
1533
+ const debugBoundaryAction = debugBoundary.take(results.length);
1534
+ let debugForcedListEnd = false;
1535
+ if (debugBoundaryAction) {
1536
+ const debugProgress = {
1537
+ debug_boundary_mode: debugBoundaryAction.mode,
1538
+ debug_boundary_threshold: debugBoundaryAction.threshold,
1539
+ debug_boundary_triggered: true,
1540
+ debug_boundary_trigger_count: debugBoundaryAction.trigger_count,
1541
+ debug_boundary_processed: debugBoundaryAction.processed
1542
+ };
1543
+ updateRecommendProgress(debugProgress);
1544
+ runControl.checkpoint({
1545
+ debug_boundary: {
1546
+ ...debugProgress,
1547
+ field: debugBoundaryAction.field,
1548
+ triggered_at: new Date().toISOString()
1549
+ }
1550
+ });
1551
+ if (debugBoundaryAction.mode === "context_recovery") {
1552
+ runControl.setPhase("recommend:debug-force-context-recovery");
1553
+ await recoverAndReapplyRecommendContext("debug_force_context_recovery_after_processed", null, {
1554
+ forceRecentNotView: true
1555
+ });
1556
+ updateRecommendProgress({
1557
+ debug_force_context_recovery_count: debugBoundaryAction.trigger_count
1558
+ });
1559
+ runControl.setPhase("recommend:list-read");
1560
+ } else if (debugBoundaryAction.mode === "cdp_reconnect") {
1561
+ debugForceReconnectPending = debugBoundaryAction;
1562
+ updateRecommendProgress({
1563
+ debug_force_cdp_reconnect_pending: true
1564
+ });
1565
+ } else if (debugBoundaryAction.mode === "list_end") {
1566
+ debugForcedListEnd = true;
1567
+ updateRecommendProgress({
1568
+ debug_force_list_end_count: debugBoundaryAction.trigger_count
1569
+ });
1570
+ }
1571
+ }
1572
+ const listReadAcquisition = await acquireRecommendListReadWithStaleRecovery({
1573
+ maxRetries: 2,
1574
+ acquire: async () => {
1575
+ await runControl.waitIfPaused();
1576
+ runControl.throwIfCanceled();
1577
+ runControl.setPhase("recommend:list-read");
1578
+ rootState = await ensureRecommendViewport(rootState, "candidate_loop");
1579
+ if (debugForcedListEnd) {
1580
+ return {
1581
+ ok: false,
1582
+ end_reached: true,
1583
+ reason: "debug_forced_list_end"
1584
+ };
1585
+ }
1586
+ return measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1587
+ client,
1588
+ state: listState,
1589
+ maxScrolls: listMaxScrolls,
1590
+ stableSignatureLimit: listStableSignatureLimit,
1591
+ wheelDeltaY: listWheelDeltaY,
1592
+ settleMs: listSettleMs,
1593
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1594
+ fallbackPoint: listFallbackResolver,
1595
+ findNodeIds: async () => {
1596
+ let currentRootState = await getRecommendRoots(client);
1597
+ currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
1598
+ rootState = currentRootState;
1599
+ if (debugForceReconnectPending) {
1600
+ runControl.setPhase("recommend:debug-force-cdp-reconnect");
1601
+ const rawClient = client.__rawClient;
1602
+ if (!rawClient || typeof rawClient.close !== "function") {
1603
+ const error = new Error("Guarded CDP client does not expose a closable __rawClient");
1604
+ error.code = "RECOMMEND_DEBUG_RAW_CDP_UNAVAILABLE";
1605
+ throw error;
1606
+ }
1607
+ const forcedReconnectAction = debugForceReconnectPending;
1608
+ debugForceReconnectPending = null;
1609
+ await rawClient.close();
1610
+ runControl.checkpoint({
1611
+ debug_cdp_reconnect: {
1612
+ raw_connection_closed: true,
1613
+ processed: forcedReconnectAction.processed,
1614
+ threshold: forcedReconnectAction.threshold,
1615
+ closed_at: new Date().toISOString()
1616
+ }
1617
+ });
1618
+ updateRecommendProgress({
1619
+ debug_force_cdp_reconnect_pending: false,
1620
+ debug_force_cdp_reconnect_count: forcedReconnectAction.trigger_count
1621
+ });
1622
+ runControl.setPhase("recommend:list-read");
1623
+ }
1624
+ const currentCardNodeIds = await waitForRecommendCardNodeIds(
1625
+ client,
1626
+ currentRootState.iframe.documentNodeId,
1627
+ {
1628
+ timeoutMs: Math.min(cardTimeoutMs, 5000),
1629
+ intervalMs: 300
1630
+ }
1631
+ );
1632
+ cardNodeIds = currentCardNodeIds;
1633
+ return currentCardNodeIds;
1634
+ },
1635
+ readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
1636
+ targetUrl,
1637
+ source: "recommend-run-card",
1638
+ metadata: {
1639
+ run_candidate_index: results.length,
1640
+ visible_index: visibleIndex
1641
+ }
1642
+ }),
1643
+ shouldRethrowReadError: isStaleRecommendNodeError,
1644
+ detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1645
+ rootNodeId: rootState?.iframe?.documentNodeId,
1646
+ markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
1647
+ refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
1648
+ textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1649
+ maxTextScanNodes: 500
1650
+ })
1651
+ }));
1652
+ },
1653
+ onStale: async ({ diagnostic }) => {
1654
+ listReadStaleRecoveryAttempts += 1;
1655
+ lastListReadStaleDiagnostic = diagnostic;
1656
+ listReadStaleDiagnostics.push(diagnostic);
1657
+ runControl.checkpoint({
1658
+ list_read_stale_recovery: {
1659
+ diagnostic,
1660
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1661
+ counters: countRecommendResultStatuses(results, { greetCount }),
1662
+ candidate_list: compactInfiniteListState(listState)
1663
+ }
1664
+ });
1665
+ updateRecommendProgress();
1666
+ },
1667
+ recover: async ({ error, diagnostic }) => {
1668
+ try {
1669
+ return await recoverRecommendListReadStaleContext({
1670
+ staleAttempt: diagnostic.attempt,
1671
+ listState,
1672
+ rootReacquire: async () => {
1673
+ await runControl.waitIfPaused();
1674
+ runControl.throwIfCanceled();
1675
+ runControl.setPhase("recommend:list-read-root-reacquire");
1676
+ let freshRootState = await getRecommendRoots(client);
1677
+ freshRootState = await ensureRecommendViewport(
1678
+ freshRootState,
1679
+ "list_read_stale_root_reacquire"
1680
+ );
1681
+ const freshCardNodeIds = await waitForRecommendCardNodeIds(
1682
+ client,
1683
+ freshRootState.iframe.documentNodeId,
1684
+ {
1685
+ timeoutMs: Math.min(cardTimeoutMs, 10000),
1686
+ intervalMs: 300
1687
+ }
1688
+ );
1689
+ rootState = freshRootState;
1690
+ cardNodeIds = freshCardNodeIds;
1691
+ return {
1692
+ card_count: freshCardNodeIds.length,
1693
+ iframe_document_node_id: freshRootState.iframe.documentNodeId
1694
+ };
1695
+ },
1696
+ contextReapply: async ({ rootReacquireError }) => {
1697
+ await runControl.waitIfPaused();
1698
+ runControl.throwIfCanceled();
1699
+ const recovery = await recoverAndReapplyRecommendContext(
1700
+ "list_read_stale_node",
1701
+ rootReacquireError || error,
1702
+ { forceRecentNotView: true }
1703
+ );
1704
+ return {
1705
+ ok: Boolean(recovery?.ok),
1706
+ method: recovery?.method || null,
1707
+ card_count: recovery?.card_count || 0
1708
+ };
1709
+ }
1710
+ });
1711
+ } catch (recoveryError) {
1712
+ updateRecommendProgress({
1713
+ list_read_stale_recovery_failed: true
1714
+ });
1715
+ runControl.checkpoint({
1716
+ list_read_stale_recovery_failed: {
1717
+ trigger: lastListReadStaleDiagnostic,
1718
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1719
+ recovery_error: compactError(recoveryError, "RECOMMEND_LIST_READ_RECOVERY_FAILED"),
1720
+ candidate_list: compactInfiniteListState(listState)
1721
+ }
1722
+ });
1723
+ throw recoveryError;
1724
+ }
1725
+ },
1726
+ onRecoveryApplied: async ({ diagnostic }) => {
1727
+ listReadStaleRecoveryApplied += 1;
1728
+ lastListReadStaleDiagnostic = diagnostic;
1729
+ lastListReadRecoveryMode = diagnostic.recovery_mode || null;
1730
+ updateRecommendProgress();
1731
+ runControl.checkpoint({
1732
+ list_read_stale_recovery_applied: {
1733
+ diagnostic,
1734
+ recovery_applied_count: listReadStaleRecoveryApplied,
1735
+ candidate_list: compactInfiniteListState(listState)
1736
+ }
1737
+ });
1738
+ },
1739
+ onRecovered: async ({ diagnostic }) => {
1740
+ listReadStaleRecoveries += 1;
1741
+ lastListReadStaleDiagnostic = diagnostic;
1742
+ updateRecommendProgress();
1743
+ runControl.checkpoint({
1744
+ list_read_stale_recovered: {
1745
+ diagnostic,
1746
+ recovery_count: listReadStaleRecoveries,
1747
+ candidate_list: compactInfiniteListState(listState)
1748
+ }
1749
+ });
1750
+ },
1751
+ onExhausted: async ({ diagnostic }) => {
1752
+ listReadStaleRecoveryAttempts += 1;
1753
+ lastListReadStaleDiagnostic = diagnostic;
1754
+ listReadStaleDiagnostics.push(diagnostic);
1755
+ updateRecommendProgress({
1756
+ list_read_stale_recovery_exhausted: true
1757
+ });
1758
+ runControl.checkpoint({
1759
+ list_read_stale_recovery_exhausted: {
1760
+ diagnostic,
1761
+ recent_diagnostics: listReadStaleDiagnostics.slice(-12),
1762
+ candidate_list: compactInfiniteListState(listState)
1763
+ }
1764
+ });
1765
+ }
1766
+ });
1767
+ const nextCandidateResult = listReadAcquisition.result;
1196
1768
  if (!nextCandidateResult.ok) {
1197
1769
  listEndReason = nextCandidateResult.reason || "list_exhausted";
1198
1770
  if (
@@ -1205,12 +1777,13 @@ export async function runRecommendWorkflow({
1205
1777
  runControl.throwIfCanceled();
1206
1778
  runControl.setPhase("recommend:refresh");
1207
1779
  refreshRounds += 1;
1208
- const refreshResult = await refreshRecommendListAtEnd(client, {
1780
+ const refreshResult = await refreshRecommendListAtEnd(client, {
1209
1781
  rootState,
1210
1782
  jobLabel,
1211
1783
  pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1212
1784
  fallbackPageScope: normalizedFallbackPageScope,
1213
1785
  filter: normalizedFilter,
1786
+ preferEndRefreshButton: debugForcedListEnd ? false : undefined,
1214
1787
  forceRecentNotView: normalizedFilter.enabled,
1215
1788
  cardTimeoutMs,
1216
1789
  buttonSettleMs: refreshButtonSettleMs,
@@ -1262,7 +1835,8 @@ export async function runRecommendWorkflow({
1262
1835
  break;
1263
1836
  }
1264
1837
 
1265
- const index = results.length;
1838
+ runControl.setPhase("recommend:candidate");
1839
+ const index = results.length;
1266
1840
  let cardNodeId = nextCandidateResult.item.node_id;
1267
1841
  const candidateKey = nextCandidateResult.item.key;
1268
1842
  let cardCandidate = nextCandidateResult.item.candidate;
@@ -1601,9 +2175,10 @@ export async function runRecommendWorkflow({
1601
2175
  greetCount += 1;
1602
2176
  }
1603
2177
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1604
- }
1605
- if (detailResult && closeDetail && !detailResult.close_result?.closed) {
1606
- detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
2178
+ }
2179
+ if (detailResult && closeDetail && !detailResult.close_result?.closed) {
2180
+ runControl.setPhase("recommend:close-detail");
2181
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1607
2182
  await maybeHumanActionCooldown("after_detail_close", timings);
1608
2183
  if (!detailResult.close_result?.closed) {
1609
2184
  closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
@@ -1746,11 +2321,20 @@ export async function runRecommendWorkflow({
1746
2321
  human_rest: humanRestController.getState(),
1747
2322
  last_human_event: lastHumanEvent,
1748
2323
  list_end_reason: listEndReason || null,
1749
- refresh_rounds: refreshRounds,
1750
- refresh_attempts: refreshAttempts,
1751
- context_recoveries: contextRecoveryAttempts,
1752
- ...countRecommendResultStatuses(results, { greetCount }),
1753
- results
2324
+ refresh_rounds: refreshRounds,
2325
+ refresh_attempts: refreshAttempts,
2326
+ context_recoveries: contextRecoveryAttempts,
2327
+ debug_boundary: debugBoundary.getState(),
2328
+ list_read_stale_recovery_attempts: listReadStaleRecoveryAttempts,
2329
+ list_read_stale_recovery_applied: listReadStaleRecoveryApplied,
2330
+ list_read_stale_recoveries: listReadStaleRecoveries,
2331
+ last_list_read_recovery_mode: lastListReadRecoveryMode,
2332
+ last_list_read_stale_diagnostic: lastListReadStaleDiagnostic,
2333
+ list_read_stale_diagnostics: listReadStaleDiagnostics.slice(-12),
2334
+ ...countRecommendResultStatuses(results, { greetCount }),
2335
+ transient_recovered: countRecommendResultStatuses(results, { greetCount }).transient_recovered
2336
+ + listReadStaleRecoveries,
2337
+ results
1754
2338
  };
1755
2339
  }
1756
2340
 
@@ -1802,11 +2386,15 @@ export function createRecommendRunService({
1802
2386
  llmImageDetail = "high",
1803
2387
  imageOutputDir = "",
1804
2388
  humanRestEnabled = false,
1805
- humanBehavior = null,
1806
- skipRecentColleagueContacted = true,
1807
- colleagueContactWindowDays = 14,
1808
- name = "recommend-domain-run"
1809
- } = {}) {
2389
+ humanBehavior = null,
2390
+ skipRecentColleagueContacted = true,
2391
+ colleagueContactWindowDays = 14,
2392
+ debugTestMode = undefined,
2393
+ debugForceListEndAfterProcessed = undefined,
2394
+ debugForceContextRecoveryAfterProcessed = undefined,
2395
+ debugForceCdpReconnectAfterProcessed = undefined,
2396
+ name = "recommend-domain-run"
2397
+ } = {}) {
1810
2398
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1811
2399
  const normalizedFilter = normalizeFilter(filter);
1812
2400
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
@@ -1818,10 +2406,16 @@ export function createRecommendRunService({
1818
2406
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1819
2407
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1820
2408
  });
1821
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1822
- const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1823
- const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1824
- return manager.startRun({
2409
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
2410
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
2411
+ const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
2412
+ const debugBoundaryOptions = normalizeRecommendDebugBoundaryOptions({
2413
+ debugTestMode: debugTestMode === true,
2414
+ debugForceListEndAfterProcessed: debugForceListEndAfterProcessed ?? null,
2415
+ debugForceContextRecoveryAfterProcessed: debugForceContextRecoveryAfterProcessed ?? null,
2416
+ debugForceCdpReconnectAfterProcessed: debugForceCdpReconnectAfterProcessed ?? null
2417
+ });
2418
+ return manager.startRun({
1825
2419
  runId,
1826
2420
  name,
1827
2421
  pid,
@@ -1864,9 +2458,12 @@ export function createRecommendRunService({
1864
2458
  colleague_contact_window_days: normalizedColleagueContactWindowDays,
1865
2459
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1866
2460
  human_behavior_profile: effectiveHumanBehavior.profile,
1867
- human_behavior: effectiveHumanBehavior,
1868
- human_rest_level: effectiveHumanBehavior.restLevel,
1869
- human_rest_enabled: effectiveHumanRestEnabled
2461
+ human_behavior: effectiveHumanBehavior,
2462
+ human_rest_level: effectiveHumanBehavior.restLevel,
2463
+ human_rest_enabled: effectiveHumanRestEnabled,
2464
+ debug_test_mode: debugBoundaryOptions.debugTestMode,
2465
+ debug_boundary_mode: debugBoundaryOptions.mode,
2466
+ debug_boundary_threshold: debugBoundaryOptions.threshold
1870
2467
  },
1871
2468
  progress: {
1872
2469
  card_count: 0,
@@ -1895,8 +2492,12 @@ export function createRecommendRunService({
1895
2492
  human_rest_level: effectiveHumanBehavior.restLevel,
1896
2493
  human_rest_enabled: effectiveHumanRestEnabled,
1897
2494
  human_rest_count: 0,
1898
- human_rest_ms: 0,
1899
- last_human_event: null
2495
+ human_rest_ms: 0,
2496
+ last_human_event: null,
2497
+ debug_boundary_mode: debugBoundaryOptions.mode,
2498
+ debug_boundary_threshold: debugBoundaryOptions.threshold,
2499
+ debug_boundary_triggered: false,
2500
+ debug_boundary_trigger_count: 0
1900
2501
  },
1901
2502
  checkpoint: {},
1902
2503
  task: (runControl) => workflow({
@@ -1937,10 +2538,14 @@ export function createRecommendRunService({
1937
2538
  llmImageDetail,
1938
2539
  imageOutputDir,
1939
2540
  humanRestEnabled: effectiveHumanRestEnabled,
1940
- humanBehavior: effectiveHumanBehavior,
1941
- skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
1942
- colleagueContactWindowDays: normalizedColleagueContactWindowDays
1943
- }, runControl)
2541
+ humanBehavior: effectiveHumanBehavior,
2542
+ skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
2543
+ colleagueContactWindowDays: normalizedColleagueContactWindowDays,
2544
+ debugTestMode: debugBoundaryOptions.debugTestMode,
2545
+ debugForceListEndAfterProcessed: debugBoundaryOptions.debugForceListEndAfterProcessed,
2546
+ debugForceContextRecoveryAfterProcessed: debugBoundaryOptions.debugForceContextRecoveryAfterProcessed,
2547
+ debugForceCdpReconnectAfterProcessed: debugBoundaryOptions.debugForceCdpReconnectAfterProcessed
2548
+ }, runControl)
1944
2549
  });
1945
2550
  }
1946
2551