@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.
- package/package.json +9 -8
- package/src/core/browser/index.js +56 -29
- package/src/core/infinite-list/index.js +31 -22
- package/src/core/run/index.js +22 -5
- package/src/domains/recommend/detail.js +13 -4
- package/src/domains/recommend/refresh.js +95 -14
- package/src/domains/recommend/run-service.js +712 -107
- package/src/index.js +108 -4
- package/src/recommend-mcp.js +392 -146
- package/src/recommend-scheduler.js +75 -9
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|