@reconcrap/boss-recommend-mcp 2.1.19 → 2.1.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.1.19",
3
+ "version": "2.1.21",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -462,6 +462,9 @@ export function createHumanRestController({
462
462
  shortRestProbability = 0.08,
463
463
  shortRestMinMs = 3000,
464
464
  shortRestMaxMs = 7000,
465
+ perCandidateRestEnabled = false,
466
+ perCandidateRestMinMs = 0,
467
+ perCandidateRestMaxMs = 0,
465
468
  batchThresholdBase = 25,
466
469
  batchThresholdJitter = 8,
467
470
  batchRestMinMs = 15000,
@@ -470,7 +473,10 @@ export function createHumanRestController({
470
473
  const nextRandom = normalizeRandom(random);
471
474
  const readNow = typeof nowFn === "function" ? nowFn : Date.now;
472
475
  const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
473
- const budgetProfile = (shortRestEnabled !== false || batchRestEnabled !== false)
476
+ const perCandidateMinMs = Math.max(0, Number(perCandidateRestMinMs) || 0);
477
+ const perCandidateMaxMs = Math.max(perCandidateMinMs, Number(perCandidateRestMaxMs) || perCandidateMinMs);
478
+ const perCandidateEnabled = enabled === true && perCandidateRestEnabled === true && perCandidateMaxMs > 0;
479
+ const budgetProfile = !perCandidateEnabled && (shortRestEnabled !== false || batchRestEnabled !== false)
474
480
  ? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
475
481
  : null;
476
482
  const nextBudgetRestInterval = () => budgetProfile
@@ -479,6 +485,9 @@ export function createHumanRestController({
479
485
  const state = {
480
486
  enabled: enabled === true,
481
487
  rest_level: normalizedRestLevel,
488
+ per_candidate_rest_enabled: perCandidateEnabled,
489
+ per_candidate_rest_min_ms: perCandidateMinMs,
490
+ per_candidate_rest_max_ms: perCandidateMaxMs,
482
491
  short_rest_enabled: enabled === true && shortRestEnabled !== false,
483
492
  batch_rest_enabled: enabled === true && batchRestEnabled !== false,
484
493
  rest_counter: 0,
@@ -569,6 +578,40 @@ export function createHumanRestController({
569
578
  }
570
579
  const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
571
580
  updateActiveElapsed();
581
+ if (state.per_candidate_rest_enabled) {
582
+ state.rest_counter += 1;
583
+ state.processed_count += 1;
584
+ state.candidates_since_last_rest += 1;
585
+ const pauseMs = Math.round(randomBetween(
586
+ nextRandom,
587
+ state.per_candidate_rest_min_ms,
588
+ state.per_candidate_rest_max_ms
589
+ ));
590
+ await sleeper(pauseMs);
591
+ state.rest_count += 1;
592
+ state.total_rest_ms += pauseMs;
593
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
594
+ const event = {
595
+ kind: "per_candidate_rest",
596
+ rest_level: normalizedRestLevel,
597
+ pause_ms: pauseMs,
598
+ processed_since_last_rest: state.candidates_since_last_rest
599
+ };
600
+ state.candidates_since_last_rest = 0;
601
+ return {
602
+ enabled: true,
603
+ rested: true,
604
+ pause_ms: pauseMs,
605
+ rest_level: normalizedRestLevel,
606
+ rest_counter: state.rest_counter,
607
+ rest_threshold: state.rest_threshold,
608
+ processed_count: state.processed_count,
609
+ active_elapsed_ms: state.active_elapsed_ms,
610
+ rest_count: state.rest_count,
611
+ total_rest_ms: state.total_rest_ms,
612
+ events: [event]
613
+ };
614
+ }
572
615
  if (budgetProfile) {
573
616
  const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
574
617
  const pauseMs = budgetEvent?.pause_ms || 0;
@@ -88,11 +88,13 @@ import {
88
88
  makeForbiddenChatResumeNavigationError,
89
89
  recoverChatShell
90
90
  } from "./page-guard.js";
91
- import { getChatRoots } from "./roots.js";
92
-
93
- const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
94
-
95
- function normalizeDetailSource(value) {
91
+ import { getChatRoots } from "./roots.js";
92
+
93
+ const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
94
+ const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS = 5000;
95
+ const CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS = 8000;
96
+
97
+ function normalizeDetailSource(value) {
96
98
  const normalized = String(value || "").trim().toLowerCase();
97
99
  return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
98
100
  }
@@ -939,13 +941,18 @@ export async function runChatWorkflow({
939
941
  humanBehavior = null
940
942
  } = {}, runControl) {
941
943
  if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
942
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
943
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
944
- });
945
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
946
- configureHumanInteraction(client, {
947
- enabled: effectiveHumanBehavior.enabled,
948
- clickMovementEnabled: effectiveHumanBehavior.clickMovement,
944
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
945
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
946
+ });
947
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
948
+ const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
949
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
950
+ const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
951
+ const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
952
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
953
+ configureHumanInteraction(client, {
954
+ enabled: effectiveHumanBehavior.enabled,
955
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
949
956
  textEntryEnabled: effectiveHumanBehavior.textEntry,
950
957
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
951
958
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
@@ -954,13 +961,12 @@ export async function runChatWorkflow({
954
961
  enabled: effectiveHumanRestEnabled,
955
962
  shortRestEnabled: effectiveHumanBehavior.shortRest,
956
963
  batchRestEnabled: effectiveHumanBehavior.batchRest,
957
- restLevel: effectiveHumanBehavior.restLevel
964
+ restLevel: effectiveHumanBehavior.restLevel,
965
+ perCandidateRestEnabled: collectCvPerCandidateRestEnabled,
966
+ perCandidateRestMinMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS,
967
+ perCandidateRestMaxMs: CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS
958
968
  });
959
- const normalizedDetailSource = normalizeDetailSource(detailSource);
960
- const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
961
- const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
962
- const useLlmScreening = normalizedScreeningMode === "llm" && !collectCvOnly;
963
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
969
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
964
970
  const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
965
971
  ? Number(targetPassCount)
966
972
  : null;
@@ -1177,11 +1183,14 @@ export async function runChatWorkflow({
1177
1183
  viewport_checks: viewportGuard.getStats().checks,
1178
1184
  viewport_recoveries: viewportGuard.getStats().recoveries,
1179
1185
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1180
- human_behavior_profile: effectiveHumanBehavior.profile,
1181
- human_rest_level: effectiveHumanBehavior.restLevel,
1182
- human_rest_enabled: effectiveHumanRestEnabled,
1183
- human_rest_count: humanRestController.getState().rest_count,
1184
- human_rest_ms: humanRestController.getState().total_rest_ms,
1186
+ human_behavior_profile: effectiveHumanBehavior.profile,
1187
+ human_rest_level: effectiveHumanBehavior.restLevel,
1188
+ human_rest_enabled: effectiveHumanRestEnabled,
1189
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1190
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1191
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1192
+ human_rest_count: humanRestController.getState().rest_count,
1193
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1185
1194
  last_human_event: lastHumanEvent
1186
1195
  });
1187
1196
  runControl.setPhase("chat:done");
@@ -1239,10 +1248,20 @@ export async function runChatWorkflow({
1239
1248
  screening_mode: normalizedScreeningMode,
1240
1249
  unique_seen: compactInfiniteListState(listState).seen_count,
1241
1250
  scroll_count: 0,
1242
- context_recoveries: contextRecoveryAttempts,
1243
- viewport_checks: viewportGuard.getStats().checks,
1244
- viewport_recoveries: viewportGuard.getStats().recoveries
1245
- });
1251
+ context_recoveries: contextRecoveryAttempts,
1252
+ viewport_checks: viewportGuard.getStats().checks,
1253
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1254
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1255
+ human_behavior_profile: effectiveHumanBehavior.profile,
1256
+ human_rest_level: effectiveHumanBehavior.restLevel,
1257
+ human_rest_enabled: effectiveHumanRestEnabled,
1258
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
1259
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
1260
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
1261
+ human_rest_count: humanRestController.getState().rest_count,
1262
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1263
+ last_human_event: lastHumanEvent
1264
+ });
1246
1265
 
1247
1266
  while (
1248
1267
  results.length < processedLimit
@@ -2027,11 +2046,14 @@ export async function runChatWorkflow({
2027
2046
  viewport_checks: viewportGuard.getStats().checks,
2028
2047
  viewport_recoveries: viewportGuard.getStats().recoveries,
2029
2048
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2030
- human_behavior_profile: effectiveHumanBehavior.profile,
2031
- human_rest_level: effectiveHumanBehavior.restLevel,
2032
- human_rest_enabled: effectiveHumanRestEnabled,
2033
- human_rest_count: humanRestController.getState().rest_count,
2034
- human_rest_ms: humanRestController.getState().total_rest_ms,
2049
+ human_behavior_profile: effectiveHumanBehavior.profile,
2050
+ human_rest_level: effectiveHumanBehavior.restLevel,
2051
+ human_rest_enabled: effectiveHumanRestEnabled,
2052
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2053
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2054
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2055
+ human_rest_count: humanRestController.getState().rest_count,
2056
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2035
2057
  last_human_event: lastHumanEvent,
2036
2058
  last_candidate_id: screeningCandidate.id || null,
2037
2059
  last_candidate_key: candidateKey,
@@ -2071,10 +2093,11 @@ export async function runChatWorkflow({
2071
2093
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
2072
2094
  compactResult.timings.total_ms = Date.now() - candidateStarted;
2073
2095
  runControl.updateProgress({
2074
- human_rest_enabled: effectiveHumanRestEnabled,
2075
- human_rest_level: effectiveHumanBehavior.restLevel,
2076
- human_rest_count: humanRestController.getState().rest_count,
2077
- human_rest_ms: humanRestController.getState().total_rest_ms,
2096
+ human_rest_enabled: effectiveHumanRestEnabled,
2097
+ human_rest_level: effectiveHumanBehavior.restLevel,
2098
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2099
+ human_rest_count: humanRestController.getState().rest_count,
2100
+ human_rest_ms: humanRestController.getState().total_rest_ms,
2078
2101
  human_rest_last: restResult,
2079
2102
  context_recoveries: contextRecoveryAttempts,
2080
2103
  last_human_event: lastHumanEvent
@@ -2175,12 +2198,14 @@ export function createChatRunService({
2175
2198
  if (!client) throw new Error("startChatRun requires a guarded CDP client");
2176
2199
  const normalizedDetailSource = normalizeDetailSource(detailSource);
2177
2200
  const normalizedScreeningMode = normalizeText(criteria) ? normalizeScreeningMode(screeningMode) : "collect_cv";
2178
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2179
- const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2180
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2181
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2182
- });
2183
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
2201
+ const collectCvOnly = normalizedScreeningMode === "collect_cv" || !normalizeText(criteria);
2202
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
2203
+ const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
2204
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
2205
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
2206
+ });
2207
+ const collectCvPerCandidateRestEnabled = collectCvOnly && effectiveHumanBehavior.enabled;
2208
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled || collectCvPerCandidateRestEnabled;
2184
2209
  return manager.startRun({
2185
2210
  runId,
2186
2211
  name,
@@ -2206,10 +2231,10 @@ export function createChatRunService({
2206
2231
  llm_configured: Boolean(llmConfig),
2207
2232
  llm_timeout_ms: llmTimeoutMs,
2208
2233
  llm_image_limit: llmImageLimit,
2209
- llm_image_detail: llmImageDetail,
2210
- max_image_pages: maxImagePages,
2211
- image_wheel_delta_y: imageWheelDeltaY,
2212
- list_max_scrolls: listMaxScrolls,
2234
+ llm_image_detail: llmImageDetail,
2235
+ max_image_pages: maxImagePages,
2236
+ image_wheel_delta_y: imageWheelDeltaY,
2237
+ list_max_scrolls: listMaxScrolls,
2213
2238
  list_stable_signature_limit: listStableSignatureLimit,
2214
2239
  list_wheel_delta_y: listWheelDeltaY,
2215
2240
  list_settle_ms: listSettleMs,
@@ -2221,7 +2246,9 @@ export function createChatRunService({
2221
2246
  human_behavior: effectiveHumanBehavior,
2222
2247
  human_rest_level: effectiveHumanBehavior.restLevel,
2223
2248
  human_rest_enabled: effectiveHumanRestEnabled,
2224
- cv_collection_mode: normalizedScreeningMode === "collect_cv"
2249
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2250
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2251
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null
2225
2252
  },
2226
2253
  progress: {
2227
2254
  card_count: 0,
@@ -2239,10 +2266,13 @@ export function createChatRunService({
2239
2266
  request_skipped: 0,
2240
2267
  context_recoveries: 0,
2241
2268
  human_behavior_enabled: effectiveHumanBehavior.enabled,
2242
- human_behavior_profile: effectiveHumanBehavior.profile,
2243
- human_rest_level: effectiveHumanBehavior.restLevel,
2244
- human_rest_enabled: effectiveHumanRestEnabled,
2245
- human_rest_count: 0,
2269
+ human_behavior_profile: effectiveHumanBehavior.profile,
2270
+ human_rest_level: effectiveHumanBehavior.restLevel,
2271
+ human_rest_enabled: effectiveHumanRestEnabled,
2272
+ human_rest_per_candidate_enabled: collectCvPerCandidateRestEnabled,
2273
+ human_rest_per_candidate_min_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MIN_MS : null,
2274
+ human_rest_per_candidate_max_ms: collectCvPerCandidateRestEnabled ? CHAT_COLLECT_CV_PER_CANDIDATE_REST_MAX_MS : null,
2275
+ human_rest_count: 0,
2246
2276
  human_rest_ms: 0,
2247
2277
  last_human_event: null
2248
2278
  },
@@ -54,6 +54,7 @@ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
54
54
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
55
55
  const DEFAULT_RECOMMEND_PORT = 9222;
56
56
  const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
57
+ const STATUS_METHOD_LOG_TAIL_LIMIT = 25;
57
58
  const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
58
59
  const RUN_MODE_ASYNC = "async";
59
60
  const REST_LEVEL_OPTIONS = ["low", "medium", "high"];
@@ -302,15 +303,23 @@ function resolveRecommendDetailLimit(args = {}, normalized = {}) {
302
303
  return requested;
303
304
  }
304
305
 
305
- function methodSummary(methodLog = []) {
306
+ function methodSummary(methodLog = []) {
306
307
  const summary = {};
307
308
  for (const entry of methodLog || []) {
308
309
  summary[entry.method] = (summary[entry.method] || 0) + 1;
309
310
  }
310
- return summary;
311
- }
312
-
313
- function clonePlain(value, fallback = null) {
311
+ return summary;
312
+ }
313
+
314
+ function compactMethodLogForStatus(methodLog = []) {
315
+ if (!Array.isArray(methodLog)) return [];
316
+ return methodLog.slice(-STATUS_METHOD_LOG_TAIL_LIMIT).map((entry) => ({
317
+ method: normalizeText(entry?.method || entry || ""),
318
+ at: normalizeText(entry?.at || "")
319
+ }));
320
+ }
321
+
322
+ function clonePlain(value, fallback = null) {
314
323
  try {
315
324
  return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
316
325
  } catch {
@@ -477,7 +486,7 @@ function secondsBetween(startedAt, endedAt) {
477
486
  return Math.max(1, Math.round((endMs - startMs) / 1000));
478
487
  }
479
488
 
480
- function normalizeLegacyProgress(progress = {}, summary = null) {
489
+ function normalizeLegacyProgress(progress = {}, summary = null) {
481
490
  const processed = Number.isInteger(progress.processed)
482
491
  ? progress.processed
483
492
  : Number.isInteger(summary?.processed)
@@ -502,12 +511,146 @@ function normalizeLegacyProgress(progress = {}, summary = null) {
502
511
  skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
503
512
  greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
504
513
  post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
505
- };
506
- }
507
-
508
- function completionReason(status) {
509
- if (status === RUN_STATUS_COMPLETED) return "completed";
510
- if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
514
+ };
515
+ }
516
+
517
+ const STATUS_COUNT_KEYS = [
518
+ "processed",
519
+ "screened",
520
+ "detail_opened",
521
+ "passed",
522
+ "skipped",
523
+ "llm_screened",
524
+ "greet_count",
525
+ "post_action_clicked",
526
+ "image_capture_failed",
527
+ "detail_open_failed",
528
+ "transient_recovered",
529
+ "colleague_contact_checked",
530
+ "recent_colleague_contact_skipped",
531
+ "colleague_contact_panel_missing",
532
+ "context_recoveries",
533
+ "human_rest_count",
534
+ "human_rest_ms",
535
+ "card_count",
536
+ "refresh_rounds"
537
+ ];
538
+
539
+ function compactPositiveInteger(value, fallback = null) {
540
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
541
+ }
542
+
543
+ function compactSmallRecord(value, fallback = null) {
544
+ const record = plainRecord(value);
545
+ if (!Object.keys(record).length) return fallback;
546
+ return clonePlain(record, fallback);
547
+ }
548
+
549
+ function compactRecommendSummaryForStatus(summary) {
550
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) return null;
551
+ const compact = {
552
+ domain: normalizeText(summary.domain || "recommend") || "recommend"
553
+ };
554
+ for (const key of STATUS_COUNT_KEYS) {
555
+ if (Number.isInteger(summary[key])) compact[key] = summary[key];
556
+ }
557
+ if (summary.target_url) compact.target_url = normalizeText(summary.target_url);
558
+ if (summary.list_end_reason) compact.list_end_reason = normalizeText(summary.list_end_reason);
559
+ if (Array.isArray(summary.results)) {
560
+ compact.results_count = summary.results.length;
561
+ } else if (Number.isInteger(summary.results_count)) {
562
+ compact.results_count = summary.results_count;
563
+ }
564
+ if (Array.isArray(summary.refresh_attempts)) {
565
+ compact.refresh_attempt_count = summary.refresh_attempts.length;
566
+ }
567
+ if (Array.isArray(summary.context_recoveries)) {
568
+ compact.context_recovery_count = summary.context_recoveries.length;
569
+ } else if (Number.isInteger(summary.context_recoveries)) {
570
+ compact.context_recoveries = summary.context_recoveries;
571
+ }
572
+ if (summary.job_selection) compact.job_selection = compactSmallRecord(summary.job_selection);
573
+ if (summary.page_scope) compact.page_scope = compactSmallRecord(summary.page_scope);
574
+ if (summary.filter) compact.filter = compactSmallRecord(summary.filter);
575
+ if (summary.candidate_list) {
576
+ const candidateList = plainRecord(summary.candidate_list);
577
+ compact.candidate_list = {
578
+ total_count: compactPositiveInteger(candidateList.total_count ?? candidateList.card_count, null),
579
+ visible_count: compactPositiveInteger(candidateList.visible_count, null),
580
+ list_end_reason: normalizeText(candidateList.list_end_reason || "")
581
+ };
582
+ }
583
+ if (summary.viewport_health?.stats) {
584
+ compact.viewport_health = {
585
+ stats: clonePlain(summary.viewport_health.stats, {})
586
+ };
587
+ }
588
+ if (summary.human_behavior) {
589
+ compact.human_behavior = {
590
+ enabled: summary.human_behavior.enabled === true,
591
+ profile: normalizeText(summary.human_behavior.profile || ""),
592
+ restLevel: normalizeText(summary.human_behavior.restLevel || summary.human_behavior.rest_level || "")
593
+ };
594
+ }
595
+ if (summary.human_rest) {
596
+ const humanRest = plainRecord(summary.human_rest);
597
+ compact.human_rest = {
598
+ enabled: humanRest.enabled === true,
599
+ restLevel: normalizeText(humanRest.restLevel || humanRest.rest_level || ""),
600
+ rest_count: compactPositiveInteger(humanRest.rest_count ?? humanRest.count, null),
601
+ total_pause_ms: compactPositiveInteger(humanRest.total_pause_ms ?? humanRest.pause_ms, null)
602
+ };
603
+ }
604
+ return compact;
605
+ }
606
+
607
+ function compactRecommendCheckpointForStatus(checkpoint) {
608
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return {};
609
+ const compact = {};
610
+ if (checkpoint.updatedAt || checkpoint.updated_at) {
611
+ compact.updatedAt = normalizeText(checkpoint.updatedAt || checkpoint.updated_at);
612
+ }
613
+ if (Array.isArray(checkpoint.results)) {
614
+ compact.results_count = checkpoint.results.length;
615
+ } else if (Number.isInteger(checkpoint.results_count)) {
616
+ compact.results_count = checkpoint.results_count;
617
+ }
618
+ for (const key of STATUS_COUNT_KEYS) {
619
+ if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
620
+ }
621
+ return compact;
622
+ }
623
+
624
+ function compactRecommendResultForStatus(result) {
625
+ if (!result || typeof result !== "object" || Array.isArray(result)) return result || null;
626
+ const compact = {
627
+ ...result
628
+ };
629
+ if (Array.isArray(result.results)) {
630
+ compact.results_count = result.results.length;
631
+ compact.results_available = result.results.length > 0;
632
+ } else if (Number.isInteger(result.results_count)) {
633
+ compact.results_count = result.results_count;
634
+ compact.results_available = result.results_available === true || result.results_count > 0;
635
+ }
636
+ delete compact.results;
637
+ return compact;
638
+ }
639
+
640
+ export function compactRecommendRunForStatus(run) {
641
+ if (!run || typeof run !== "object" || Array.isArray(run)) return run || null;
642
+ const compact = {
643
+ ...run
644
+ };
645
+ if (compact.result) compact.result = compactRecommendResultForStatus(compact.result);
646
+ if (compact.summary) compact.summary = compactRecommendSummaryForStatus(compact.summary);
647
+ if (compact.checkpoint) compact.checkpoint = compactRecommendCheckpointForStatus(compact.checkpoint);
648
+ return compact;
649
+ }
650
+
651
+ function completionReason(status) {
652
+ if (status === RUN_STATUS_COMPLETED) return "completed";
653
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
511
654
  if (status === RUN_STATUS_FAILED) return "failed";
512
655
  if (status === RUN_STATUS_PAUSED) return "paused";
513
656
  return null;
@@ -685,13 +828,14 @@ function buildLegacyRecommendResult(snapshot) {
685
828
  effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
686
829
  },
687
830
  search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
688
- screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
689
- target_count_semantics: TARGET_COUNT_SEMANTICS,
690
- error: snapshot.error || null,
691
- recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
692
- results: resultRows
693
- };
694
- }
831
+ screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
832
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
833
+ error: snapshot.error || null,
834
+ recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
835
+ results_count: resultRows.length,
836
+ results_available: resultRows.length > 0
837
+ };
838
+ }
695
839
 
696
840
  function normalizeRunSnapshot(snapshot) {
697
841
  if (!snapshot) return null;
@@ -714,10 +858,12 @@ function normalizeRunSnapshot(snapshot) {
714
858
  follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
715
859
  target_count_semantics: TARGET_COUNT_SEMANTICS
716
860
  };
717
- return {
718
- ...snapshot,
719
- progress,
720
- run_id: snapshot.runId,
861
+ return {
862
+ ...snapshot,
863
+ checkpoint: compactRecommendCheckpointForStatus(snapshot.checkpoint),
864
+ summary: compactRecommendSummaryForStatus(summary),
865
+ progress,
866
+ run_id: snapshot.runId,
721
867
  mode: RUN_MODE_ASYNC,
722
868
  state: snapshot.status,
723
869
  stage: snapshot.phase,
@@ -874,7 +1020,7 @@ function persistRecommendRunSnapshot(snapshot, {
874
1020
  normalized.control = mergePersistedControlRequest(normalized, existing);
875
1021
  normalized = coerceCanceledTerminalSnapshot(normalized, existing);
876
1022
  if (persistActiveCheckpoint) {
877
- persistRecommendCheckpointSnapshot(normalized);
1023
+ persistRecommendCheckpointSnapshot(snapshot);
878
1024
  }
879
1025
  const payload = {
880
1026
  run_id: normalized.run_id,
@@ -970,17 +1116,19 @@ function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
970
1116
  });
971
1117
  }
972
1118
 
973
- function attachMethodEvidence(payload, runId) {
974
- const meta = getRecommendRunMeta(runId);
975
- assertNoForbiddenCdpCalls(meta.methodLog || []);
976
- return {
977
- ...payload,
978
- runtime_evaluate_used: false,
979
- method_summary: methodSummary(meta.methodLog || []),
980
- method_log: meta.methodLog || [],
981
- chrome: meta.chrome || null
982
- };
983
- }
1119
+ function attachMethodEvidence(payload, runId) {
1120
+ const meta = getRecommendRunMeta(runId);
1121
+ const methodLog = meta.methodLog || [];
1122
+ assertNoForbiddenCdpCalls(methodLog);
1123
+ return {
1124
+ ...payload,
1125
+ runtime_evaluate_used: false,
1126
+ method_summary: methodSummary(methodLog),
1127
+ method_log: compactMethodLogForStatus(methodLog),
1128
+ method_log_total: Array.isArray(methodLog) ? methodLog.length : 0,
1129
+ chrome: meta.chrome || null
1130
+ };
1131
+ }
984
1132
 
985
1133
  function compactRecommendJobListOption(option, index) {
986
1134
  const label = normalizeText(option?.label);
@@ -1945,19 +2093,19 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
1945
2093
  };
1946
2094
  }
1947
2095
  try {
1948
- const run = recommendRunService.getRecommendRun(runId);
1949
- const normalizedRun = persistRecommendRunSnapshot(run);
1950
- return attachMethodEvidence({
1951
- status: "RUN_STATUS",
1952
- run: normalizedRun
1953
- }, runId);
1954
- } catch {
1955
- const persisted = readRecommendRunState(runId);
1956
- if (persisted) {
1957
- const reconciled = reconcilePersistedRecommendRunIfNeeded(persisted);
1958
- return {
1959
- status: "RUN_STATUS",
1960
- run: reconciled,
2096
+ const run = recommendRunService.getRecommendRun(runId);
2097
+ const normalizedRun = persistRecommendRunSnapshot(run);
2098
+ return attachMethodEvidence({
2099
+ status: "RUN_STATUS",
2100
+ run: compactRecommendRunForStatus(normalizedRun)
2101
+ }, runId);
2102
+ } catch {
2103
+ const persisted = readRecommendRunState(runId);
2104
+ if (persisted) {
2105
+ const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2106
+ return {
2107
+ status: "RUN_STATUS",
2108
+ run: reconciled,
1961
2109
  persistence: {
1962
2110
  source: "disk",
1963
2111
  active_control_available: false,
@@ -1985,36 +2133,36 @@ export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
1985
2133
  try {
1986
2134
  const before = recommendRunService.getRecommendRun(runId);
1987
2135
  if (TERMINAL_STATUSES.has(before.status)) {
1988
- const normalizedBefore = persistRecommendRunSnapshot(before);
1989
- return attachMethodEvidence({
1990
- status: "PAUSE_IGNORED",
1991
- run: normalizedBefore,
1992
- message: "目标任务已结束,无需暂停。"
1993
- }, runId);
2136
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2137
+ return attachMethodEvidence({
2138
+ status: "PAUSE_IGNORED",
2139
+ run: compactRecommendRunForStatus(normalizedBefore),
2140
+ message: "目标任务已结束,无需暂停。"
2141
+ }, runId);
1994
2142
  }
1995
2143
  if (before.status === RUN_STATUS_PAUSED) {
1996
- const normalizedBefore = persistRecommendRunSnapshot(before);
1997
- return attachMethodEvidence({
1998
- status: "PAUSE_IGNORED",
1999
- run: normalizedBefore,
2000
- message: "目标任务已经处于 paused 状态。"
2001
- }, runId);
2144
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2145
+ return attachMethodEvidence({
2146
+ status: "PAUSE_IGNORED",
2147
+ run: compactRecommendRunForStatus(normalizedBefore),
2148
+ message: "目标任务已经处于 paused 状态。"
2149
+ }, runId);
2002
2150
  }
2003
2151
  const run = recommendRunService.pauseRecommendRun(runId);
2004
- const normalizedRun = persistRecommendRunSnapshot(run);
2005
- return attachMethodEvidence({
2006
- status: "PAUSE_REQUESTED",
2007
- run: normalizedRun,
2008
- message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2009
- }, runId);
2152
+ const normalizedRun = persistRecommendRunSnapshot(run);
2153
+ return attachMethodEvidence({
2154
+ status: "PAUSE_REQUESTED",
2155
+ run: compactRecommendRunForStatus(normalizedRun),
2156
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2157
+ }, runId);
2010
2158
  } catch {
2011
2159
  const persisted = readRecommendRunState(runId);
2012
2160
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2013
- return {
2014
- status: "PAUSE_IGNORED",
2015
- run: persisted,
2016
- message: "目标任务已结束,无需暂停。",
2017
- runtime_evaluate_used: false,
2161
+ return {
2162
+ status: "PAUSE_IGNORED",
2163
+ run: compactRecommendRunForStatus(persisted),
2164
+ message: "目标任务已结束,无需暂停。",
2165
+ runtime_evaluate_used: false,
2018
2166
  method_summary: {},
2019
2167
  method_log: [],
2020
2168
  chrome: null
@@ -2029,28 +2177,28 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2029
2177
  try {
2030
2178
  const before = recommendRunService.getRecommendRun(runId);
2031
2179
  if (TERMINAL_STATUSES.has(before.status)) {
2032
- const normalizedBefore = persistRecommendRunSnapshot(before);
2033
- return attachMethodEvidence({
2034
- status: "FAILED",
2180
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2181
+ return attachMethodEvidence({
2182
+ status: "FAILED",
2035
2183
  error: {
2036
2184
  code: "RUN_ALREADY_TERMINATED",
2037
2185
  message: "目标任务已结束,无法继续。",
2038
2186
  retryable: false
2039
2187
  },
2040
- run: normalizedBefore
2041
- }, runId);
2188
+ run: compactRecommendRunForStatus(normalizedBefore)
2189
+ }, runId);
2042
2190
  }
2043
2191
  if (before.status !== RUN_STATUS_PAUSED) {
2044
- const normalizedBefore = persistRecommendRunSnapshot(before);
2045
- return attachMethodEvidence({
2046
- status: "FAILED",
2192
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2193
+ return attachMethodEvidence({
2194
+ status: "FAILED",
2047
2195
  error: {
2048
2196
  code: "RUN_NOT_PAUSED",
2049
2197
  message: "仅 paused 状态的 run 才能继续。",
2050
2198
  retryable: true
2051
2199
  },
2052
- run: normalizedBefore
2053
- }, runId);
2200
+ run: compactRecommendRunForStatus(normalizedBefore)
2201
+ }, runId);
2054
2202
  }
2055
2203
  const run = recommendRunService.resumeRecommendRun(runId);
2056
2204
  const meta = getRecommendRunMeta(runId);
@@ -2058,12 +2206,12 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2058
2206
  meta.resumeCount = (meta.resumeCount || 0) + 1;
2059
2207
  meta.lastResumedAt = new Date().toISOString();
2060
2208
  }
2061
- const normalizedRun = persistRecommendRunSnapshot(run);
2062
- return attachMethodEvidence({
2063
- status: "RESUME_REQUESTED",
2064
- run: normalizedRun,
2065
- poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
2066
- message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2209
+ const normalizedRun = persistRecommendRunSnapshot(run);
2210
+ return attachMethodEvidence({
2211
+ status: "RESUME_REQUESTED",
2212
+ run: compactRecommendRunForStatus(normalizedRun),
2213
+ poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
2214
+ message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2067
2215
  }, runId);
2068
2216
  } catch {
2069
2217
  const persisted = readRecommendRunState(runId);
@@ -2077,9 +2225,9 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2077
2225
  : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
2078
2226
  retryable: !TERMINAL_STATUSES.has(persisted.state)
2079
2227
  },
2080
- run: persisted,
2081
- persistence: {
2082
- source: "disk",
2228
+ run: compactRecommendRunForStatus(persisted),
2229
+ persistence: {
2230
+ source: "disk",
2083
2231
  active_control_available: false
2084
2232
  },
2085
2233
  runtime_evaluate_used: false,
@@ -2097,28 +2245,28 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2097
2245
  try {
2098
2246
  const before = recommendRunService.getRecommendRun(runId);
2099
2247
  if (TERMINAL_STATUSES.has(before.status)) {
2100
- const normalizedBefore = persistRecommendRunSnapshot(before);
2101
- return attachMethodEvidence({
2102
- status: "CANCEL_IGNORED",
2103
- run: normalizedBefore,
2104
- message: "目标任务已结束,无需取消。"
2105
- }, runId);
2248
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2249
+ return attachMethodEvidence({
2250
+ status: "CANCEL_IGNORED",
2251
+ run: compactRecommendRunForStatus(normalizedBefore),
2252
+ message: "目标任务已结束,无需取消。"
2253
+ }, runId);
2106
2254
  }
2107
2255
  const run = recommendRunService.cancelRecommendRun(runId);
2108
- const normalizedRun = persistRecommendRunSnapshot(run);
2109
- return attachMethodEvidence({
2110
- status: "CANCEL_REQUESTED",
2111
- run: normalizedRun,
2112
- message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2113
- }, runId);
2256
+ const normalizedRun = persistRecommendRunSnapshot(run);
2257
+ return attachMethodEvidence({
2258
+ status: "CANCEL_REQUESTED",
2259
+ run: compactRecommendRunForStatus(normalizedRun),
2260
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2261
+ }, runId);
2114
2262
  } catch {
2115
2263
  const persisted = readRecommendRunState(runId);
2116
2264
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2117
2265
  return {
2118
2266
  status: "CANCEL_IGNORED",
2119
- run: persisted,
2267
+ run: compactRecommendRunForStatus(persisted),
2120
2268
  message: "目标任务已结束,无需取消。",
2121
- runtime_evaluate_used: false,
2269
+ runtime_evaluate_used: false,
2122
2270
  method_summary: {},
2123
2271
  method_log: [],
2124
2272
  chrome: null
@@ -2136,7 +2284,7 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2136
2284
  if (patched) {
2137
2285
  return {
2138
2286
  status: "CANCEL_REQUESTED",
2139
- run: patched,
2287
+ run: compactRecommendRunForStatus(patched),
2140
2288
  message: cancelMessage,
2141
2289
  persistence: {
2142
2290
  source: "disk",
@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getStateHome } from "./run-state.js";
7
7
  import {
8
+ compactRecommendRunForStatus,
8
9
  getRecommendPipelineRunTool,
9
10
  prepareRecommendPipelineRunTool,
10
11
  startRecommendPipelineRunTool
@@ -142,6 +143,19 @@ function writeSchedule(scheduleId, patch) {
142
143
  });
143
144
  }
144
145
 
146
+ function compactScheduleForStatus(schedule) {
147
+ if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
148
+ const compact = clonePlain(schedule, schedule);
149
+ if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
150
+ if (compact.launch_payload?.run) {
151
+ compact.launch_payload = {
152
+ ...compact.launch_payload,
153
+ run: compactRecommendRunForStatus(compact.launch_payload.run)
154
+ };
155
+ }
156
+ return compact;
157
+ }
158
+
145
159
  function isProcessAlive(pid) {
146
160
  if (!Number.isInteger(pid) || pid <= 0) return false;
147
161
  try {
@@ -355,7 +369,7 @@ export function getRecommendScheduledRunTool({ args = {} } = {}) {
355
369
  return {
356
370
  status: "OK",
357
371
  schedule_id: scheduleId,
358
- schedule: next
372
+ schedule: compactScheduleForStatus(next)
359
373
  };
360
374
  }
361
375