@reconcrap/boss-recommend-mcp 2.0.56 → 2.1.0

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/src/cli.js CHANGED
@@ -63,12 +63,13 @@ const installConfigDefaults = Object.freeze({
63
63
  llmMaxRetries: 3,
64
64
  llmImageLimit: 8,
65
65
  llmImageDetail: "low",
66
- humanRestEnabled: true,
67
- humanBehavior: {
68
- enabled: true,
69
- profile: "paced_with_rests"
70
- }
71
- });
66
+ humanRestEnabled: true,
67
+ humanBehavior: {
68
+ enabled: true,
69
+ profile: "paced_with_rests",
70
+ restLevel: "low"
71
+ }
72
+ });
72
73
  const bossChatRuntimeChildDirs = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
73
74
  const bossChatCliUnsupportedStartCode = "CHAT_CLI_ASYNC_UNSUPPORTED_CDP_ONLY";
74
75
  const calibrateUnsupportedCode = "CALIBRATE_UNSUPPORTED_CDP_ONLY";
@@ -613,14 +614,20 @@ function parseBossChatTargetCountOption(raw) {
613
614
  return parsed ?? text;
614
615
  }
615
616
 
616
- function parseBooleanOption(raw, fallback = undefined) {
617
- if (raw === undefined || raw === null || raw === "") return fallback;
618
- if (raw === true) return true;
619
- const normalized = String(raw).trim().toLowerCase();
620
- if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
621
- if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
622
- return fallback;
623
- }
617
+ function parseBooleanOption(raw, fallback = undefined) {
618
+ if (raw === undefined || raw === null || raw === "") return fallback;
619
+ if (raw === true) return true;
620
+ const normalized = String(raw).trim().toLowerCase();
621
+ if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
622
+ if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
623
+ return fallback;
624
+ }
625
+
626
+ function parseRestLevelOption(raw) {
627
+ if (raw === undefined || raw === null || raw === "") return undefined;
628
+ const normalized = String(raw).trim().toLowerCase();
629
+ return ["low", "medium", "high"].includes(normalized) ? normalized : undefined;
630
+ }
624
631
 
625
632
  function normalizePageScope(value) {
626
633
  const normalized = String(value || "").trim().toLowerCase();
@@ -1271,20 +1278,33 @@ async function resolveCliConfigTarget(options = {}) {
1271
1278
  };
1272
1279
  }
1273
1280
 
1274
- function applyMissingInstallConfigDefaults(config = {}) {
1275
- const nextConfig = { ...config };
1276
- const patchedKeys = [];
1277
- for (const [key, defaultValue] of Object.entries(installConfigDefaults)) {
1281
+ function applyMissingInstallConfigDefaults(config = {}) {
1282
+ const nextConfig = { ...config };
1283
+ const patchedKeys = [];
1284
+ for (const [key, defaultValue] of Object.entries(installConfigDefaults)) {
1278
1285
  if (!Object.prototype.hasOwnProperty.call(nextConfig, key)) {
1279
1286
  nextConfig[key] = defaultValue;
1280
- patchedKeys.push(key);
1281
- }
1282
- }
1283
- return {
1284
- nextConfig,
1285
- patchedKeys
1286
- };
1287
- }
1287
+ patchedKeys.push(key);
1288
+ }
1289
+ }
1290
+ if (
1291
+ nextConfig.humanBehavior
1292
+ && typeof nextConfig.humanBehavior === "object"
1293
+ && !Array.isArray(nextConfig.humanBehavior)
1294
+ && !Object.prototype.hasOwnProperty.call(nextConfig.humanBehavior, "restLevel")
1295
+ && !Object.prototype.hasOwnProperty.call(nextConfig.humanBehavior, "rest_level")
1296
+ ) {
1297
+ nextConfig.humanBehavior = {
1298
+ ...nextConfig.humanBehavior,
1299
+ restLevel: "low"
1300
+ };
1301
+ patchedKeys.push("humanBehavior.restLevel");
1302
+ }
1303
+ return {
1304
+ nextConfig,
1305
+ patchedKeys
1306
+ };
1307
+ }
1288
1308
 
1289
1309
  async function ensureUserConfig(options = {}) {
1290
1310
  const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
@@ -2721,19 +2741,30 @@ async function runPipelineOnce(options = {}) {
2721
2741
  const workspaceRoot = getWorkspaceRoot(options);
2722
2742
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
2723
2743
 
2724
- const args = {
2725
- instruction,
2726
- confirmation: confirmation ?? undefined,
2727
- overrides: overrides ?? undefined,
2728
- follow_up: followUp ?? undefined,
2744
+ const args = {
2745
+ instruction,
2746
+ confirmation: confirmation ?? undefined,
2747
+ overrides: overrides ?? undefined,
2748
+ follow_up: followUp ?? undefined,
2729
2749
  host: typeof options.host === "string" && options.host.trim() ? options.host.trim() : undefined,
2730
2750
  port,
2731
2751
  target_url_includes: typeof options["target-url-includes"] === "string" && options["target-url-includes"].trim()
2732
2752
  ? options["target-url-includes"].trim()
2733
2753
  : undefined,
2734
2754
  allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2735
- slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2736
- };
2755
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2756
+ };
2757
+ const restLevel = parseRestLevelOption(
2758
+ options["rest-level"]
2759
+ ?? options.rest_level
2760
+ ?? options["human-behavior-rest-level"]
2761
+ ?? options.human_behavior_rest_level
2762
+ );
2763
+ if (restLevel) {
2764
+ args.human_behavior = {
2765
+ restLevel
2766
+ };
2767
+ }
2737
2768
 
2738
2769
  const optionalPassthrough = [
2739
2770
  "detail_limit",
@@ -2810,16 +2841,22 @@ async function listRecommendJobsCli(options = {}) {
2810
2841
  }));
2811
2842
  }
2812
2843
 
2813
- function buildBossChatCliInput(options = {}) {
2814
- const greetingTextRaw =
2815
- options["greeting-text"]
2816
- ?? options.greeting_text
2817
- ?? options.greetingText
2818
- ?? options.greeting;
2819
- const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2820
- const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2821
- const host = String(options.host || "").trim();
2822
- return {
2844
+ function buildBossChatCliInput(options = {}) {
2845
+ const greetingTextRaw =
2846
+ options["greeting-text"]
2847
+ ?? options.greeting_text
2848
+ ?? options.greetingText
2849
+ ?? options.greeting;
2850
+ const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2851
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2852
+ const host = String(options.host || "").trim();
2853
+ const restLevel = parseRestLevelOption(
2854
+ options["rest-level"]
2855
+ ?? options.rest_level
2856
+ ?? options["human-behavior-rest-level"]
2857
+ ?? options.human_behavior_rest_level
2858
+ );
2859
+ return {
2823
2860
  profile: typeof options.profile === "string" ? options.profile.trim() : undefined,
2824
2861
  job: typeof options.job === "string" ? options.job.trim() : undefined,
2825
2862
  start_from: String(options["start-from"] || options.start_from || "").trim().toLowerCase() || undefined,
@@ -2837,13 +2874,18 @@ function buildBossChatCliInput(options = {}) {
2837
2874
  dry_run: options["dry-run"] === true || options.dryRun === true,
2838
2875
  no_state: options["no-state"] === true || options.noState === true,
2839
2876
  human_behavior_enabled: parseBooleanOption(options["human-behavior-enabled"] ?? options.human_behavior_enabled),
2840
- human_behavior_profile: typeof (options["human-behavior-profile"] ?? options.human_behavior_profile) === "string"
2841
- ? (options["human-behavior-profile"] ?? options.human_behavior_profile).trim()
2842
- : undefined,
2843
- safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
2844
- batch_rest_enabled: parseBooleanOption(options["batch-rest"] ?? options.batch_rest_enabled)
2845
- };
2846
- }
2877
+ human_behavior_profile: typeof (options["human-behavior-profile"] ?? options.human_behavior_profile) === "string"
2878
+ ? (options["human-behavior-profile"] ?? options.human_behavior_profile).trim()
2879
+ : undefined,
2880
+ human_behavior: restLevel
2881
+ ? {
2882
+ restLevel
2883
+ }
2884
+ : undefined,
2885
+ safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
2886
+ batch_rest_enabled: parseBooleanOption(options["batch-rest"] ?? options.batch_rest_enabled)
2887
+ };
2888
+ }
2847
2889
 
2848
2890
  function getBossChatCliRunTarget(options = {}) {
2849
2891
  return {
@@ -86,6 +86,46 @@ const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
86
86
  rests: "paced_with_rests",
87
87
  rest: "paced_with_rests"
88
88
  });
89
+ const DEFAULT_HUMAN_REST_LEVEL = "low";
90
+ const HUMAN_REST_LEVEL_ALIASES = Object.freeze({
91
+ default: "low",
92
+ light: "low",
93
+ normal: "medium",
94
+ med: "medium",
95
+ heavy: "high"
96
+ });
97
+ const HUMAN_REST_LEVEL_PROFILES = Object.freeze({
98
+ medium: Object.freeze({
99
+ targetRestMs: 30 * 60 * 1000,
100
+ targetCandidateCount: 700,
101
+ targetWindowMs: 5 * 60 * 60 * 1000,
102
+ intervalMin: 4,
103
+ intervalMax: 16,
104
+ longRestProbability: 0.22,
105
+ shortRestMinMs: 8000,
106
+ shortRestMaxMs: 45000,
107
+ longRestMinMs: 60000,
108
+ longRestMaxMs: 180000,
109
+ minDebtToRestMs: 8000,
110
+ forceDebtMs: 90000,
111
+ maxOverspendMs: 15000
112
+ }),
113
+ high: Object.freeze({
114
+ targetRestMs: 60 * 60 * 1000,
115
+ targetCandidateCount: 700,
116
+ targetWindowMs: 5 * 60 * 60 * 1000,
117
+ intervalMin: 3,
118
+ intervalMax: 12,
119
+ longRestProbability: 0.28,
120
+ shortRestMinMs: 12000,
121
+ shortRestMaxMs: 75000,
122
+ longRestMinMs: 90000,
123
+ longRestMaxMs: 300000,
124
+ minDebtToRestMs: 12000,
125
+ forceDebtMs: 150000,
126
+ maxOverspendMs: 25000
127
+ })
128
+ });
89
129
 
90
130
  function clampNumber(value, min, max) {
91
131
  const number = Number(value);
@@ -152,6 +192,14 @@ export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
152
192
  : fallback;
153
193
  }
154
194
 
195
+ export function normalizeHumanRestLevel(raw, fallback = DEFAULT_HUMAN_REST_LEVEL) {
196
+ const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
197
+ const level = HUMAN_REST_LEVEL_ALIASES[normalized] || normalized;
198
+ return level === "low" || level === "medium" || level === "high"
199
+ ? level
200
+ : fallback;
201
+ }
202
+
155
203
  export function normalizeHumanBehaviorOptions(raw = null, {
156
204
  legacyEnabled = false,
157
205
  safePacing = null,
@@ -231,6 +279,10 @@ export function normalizeHumanBehaviorOptions(raw = null, {
231
279
  readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
232
280
  profileDefaults.batchRest
233
281
  );
282
+ const restLevel = normalizeHumanRestLevel(
283
+ readFirstOption(rawObject, ["restLevel", "rest_level"]),
284
+ DEFAULT_HUMAN_REST_LEVEL
285
+ );
234
286
  if (batchRestFlag !== null) {
235
287
  batchRest = batchRestFlag;
236
288
  if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
@@ -248,6 +300,7 @@ export function normalizeHumanBehaviorOptions(raw = null, {
248
300
  shortRest: enabled && shortRest === true,
249
301
  batchRest: enabled && batchRest === true,
250
302
  actionCooldown: enabled && actionCooldown === true,
303
+ restLevel,
251
304
  restEnabled: enabled && (shortRest === true || batchRest === true)
252
305
  };
253
306
  }
@@ -404,6 +457,8 @@ export function createHumanRestController({
404
457
  shortRestEnabled = true,
405
458
  batchRestEnabled = true,
406
459
  random = Math.random,
460
+ nowFn = Date.now,
461
+ restLevel = DEFAULT_HUMAN_REST_LEVEL,
407
462
  shortRestProbability = 0.08,
408
463
  shortRestMinMs = 3000,
409
464
  shortRestMaxMs = 7000,
@@ -413,12 +468,26 @@ export function createHumanRestController({
413
468
  batchRestMaxMs = 30000
414
469
  } = {}) {
415
470
  const nextRandom = normalizeRandom(random);
471
+ const readNow = typeof nowFn === "function" ? nowFn : Date.now;
472
+ const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
473
+ const budgetProfile = (shortRestEnabled !== false || batchRestEnabled !== false)
474
+ ? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
475
+ : null;
476
+ const nextBudgetRestInterval = () => budgetProfile
477
+ ? randomIntegerBetween(nextRandom, budgetProfile.intervalMin, budgetProfile.intervalMax)
478
+ : 0;
416
479
  const state = {
417
480
  enabled: enabled === true,
481
+ rest_level: normalizedRestLevel,
418
482
  short_rest_enabled: enabled === true && shortRestEnabled !== false,
419
483
  batch_rest_enabled: enabled === true && batchRestEnabled !== false,
420
484
  rest_counter: 0,
421
485
  rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
486
+ processed_count: 0,
487
+ candidates_since_last_rest: 0,
488
+ candidates_until_next_rest: nextBudgetRestInterval(),
489
+ active_elapsed_ms: 0,
490
+ last_active_at_ms: Number(readNow()) || 0,
422
491
  rest_count: 0,
423
492
  total_rest_ms: 0
424
493
  };
@@ -427,6 +496,67 @@ export function createHumanRestController({
427
496
  state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
428
497
  }
429
498
 
499
+ function updateActiveElapsed() {
500
+ const now = Number(readNow()) || 0;
501
+ if (state.last_active_at_ms >= 0 && now >= state.last_active_at_ms) {
502
+ state.active_elapsed_ms += now - state.last_active_at_ms;
503
+ }
504
+ state.last_active_at_ms = now;
505
+ return now;
506
+ }
507
+
508
+ function getBudgetTargetMs() {
509
+ if (!budgetProfile) return 0;
510
+ const candidateTarget = state.processed_count * (budgetProfile.targetRestMs / budgetProfile.targetCandidateCount);
511
+ const elapsedTarget = state.active_elapsed_ms * (budgetProfile.targetRestMs / budgetProfile.targetWindowMs);
512
+ return Math.max(candidateTarget, elapsedTarget);
513
+ }
514
+
515
+ function chooseBudgetRestPause(debtMs) {
516
+ const longRest = nextRandom() < budgetProfile.longRestProbability;
517
+ const minMs = longRest ? budgetProfile.longRestMinMs : budgetProfile.shortRestMinMs;
518
+ const maxMs = longRest ? budgetProfile.longRestMaxMs : budgetProfile.shortRestMaxMs;
519
+ const scaleMin = longRest ? 0.75 : 0.38;
520
+ const scaleMax = longRest ? 1.1 : 0.78;
521
+ const desiredMs = debtMs * randomBetween(nextRandom, scaleMin, scaleMax);
522
+ const randomizedMs = randomBetween(nextRandom, minMs, maxMs);
523
+ const blendedMs = Math.max(minMs, Math.min(maxMs, (desiredMs + randomizedMs) / 2));
524
+ const maxAllowedMs = Math.max(minMs, debtMs + budgetProfile.maxOverspendMs);
525
+ return {
526
+ pauseMs: Math.round(Math.min(blendedMs, maxAllowedMs)),
527
+ restSize: longRest ? "long" : "short"
528
+ };
529
+ }
530
+
531
+ async function takeBudgetBreakIfNeeded(sleeper) {
532
+ state.processed_count += 1;
533
+ state.candidates_since_last_rest += 1;
534
+ state.candidates_until_next_rest -= 1;
535
+ const debtMs = getBudgetTargetMs() - state.total_rest_ms;
536
+ const intervalDue = state.candidates_until_next_rest <= 0;
537
+ const forceDue = debtMs >= budgetProfile.forceDebtMs;
538
+ if (!intervalDue && !forceDue) {
539
+ return null;
540
+ }
541
+ if (debtMs < budgetProfile.minDebtToRestMs) {
542
+ if (intervalDue) state.candidates_until_next_rest = nextBudgetRestInterval();
543
+ return null;
544
+ }
545
+ const { pauseMs, restSize } = chooseBudgetRestPause(debtMs);
546
+ await sleeper(pauseMs);
547
+ const event = {
548
+ kind: "random_rest",
549
+ rest_level: normalizedRestLevel,
550
+ rest_size: restSize,
551
+ pause_ms: pauseMs,
552
+ processed_since_last_rest: state.candidates_since_last_rest,
553
+ rest_budget_debt_ms: Math.round(Math.max(0, debtMs))
554
+ };
555
+ state.candidates_since_last_rest = 0;
556
+ state.candidates_until_next_rest = nextBudgetRestInterval();
557
+ return event;
558
+ }
559
+
430
560
  async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
431
561
  if (!state.enabled) {
432
562
  return {
@@ -438,18 +568,44 @@ export function createHumanRestController({
438
568
  };
439
569
  }
440
570
  const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
571
+ updateActiveElapsed();
572
+ if (budgetProfile) {
573
+ const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
574
+ const pauseMs = budgetEvent?.pause_ms || 0;
575
+ if (pauseMs > 0) {
576
+ state.rest_count += 1;
577
+ state.total_rest_ms += pauseMs;
578
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
579
+ }
580
+ return {
581
+ enabled: true,
582
+ rested: Boolean(budgetEvent),
583
+ pause_ms: pauseMs,
584
+ rest_level: normalizedRestLevel,
585
+ rest_counter: state.rest_counter,
586
+ rest_threshold: state.rest_threshold,
587
+ processed_count: state.processed_count,
588
+ candidates_until_next_rest: state.candidates_until_next_rest,
589
+ active_elapsed_ms: state.active_elapsed_ms,
590
+ rest_count: state.rest_count,
591
+ total_rest_ms: state.total_rest_ms,
592
+ events: budgetEvent ? [budgetEvent] : []
593
+ };
594
+ }
441
595
  state.rest_counter += 1;
596
+ state.processed_count += 1;
442
597
  const events = [];
443
598
  if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
444
599
  const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
445
600
  await sleeper(pauseMs);
446
- events.push({ kind: "random_rest", pause_ms: pauseMs });
601
+ events.push({ kind: "random_rest", rest_level: normalizedRestLevel, pause_ms: pauseMs });
447
602
  }
448
603
  if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
449
604
  const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
450
605
  await sleeper(pauseMs);
451
606
  events.push({
452
607
  kind: "batch_rest",
608
+ rest_level: normalizedRestLevel,
453
609
  pause_ms: pauseMs,
454
610
  processed_since_last_batch_rest: state.rest_counter
455
611
  });
@@ -460,13 +616,17 @@ export function createHumanRestController({
460
616
  if (pauseMs > 0) {
461
617
  state.rest_count += events.length;
462
618
  state.total_rest_ms += pauseMs;
619
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
463
620
  }
464
621
  return {
465
622
  enabled: true,
466
623
  rested: events.length > 0,
467
624
  pause_ms: pauseMs,
625
+ rest_level: normalizedRestLevel,
468
626
  rest_counter: state.rest_counter,
469
627
  rest_threshold: state.rest_threshold,
628
+ processed_count: state.processed_count,
629
+ active_elapsed_ms: state.active_elapsed_ms,
470
630
  rest_count: state.rest_count,
471
631
  total_rest_ms: state.total_rest_ms,
472
632
  events
@@ -743,6 +903,7 @@ export function buildBossChromeLaunchArgs({
743
903
  ...LID_CLOSED_SAFE_CHROME_ARGS,
744
904
  ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
745
905
  ...extraArgs,
906
+ "--start-maximized",
746
907
  "--new-window",
747
908
  url
748
909
  ];
@@ -326,7 +326,7 @@ export async function toggleWindowStateForViewportRecovery(client, {
326
326
  const currentInfo = await getCurrentWindowInfo(client);
327
327
  const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
328
328
  const sequence = currentState === "normal"
329
- ? ["maximized", "normal"]
329
+ ? ["maximized"]
330
330
  : ["normal", "maximized"];
331
331
  const attempts = [];
332
332
 
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ markBossChatDetachedWorkerFailed,
4
+ runBossChatDetachedWorker
5
+ } from "./chat-mcp.js";
6
+ import {
7
+ markBossRecruitDetachedWorkerFailed,
8
+ runBossRecruitDetachedWorker
9
+ } from "./recruit-mcp.js";
10
+
11
+ function normalizeText(value) {
12
+ return String(value || "").replace(/\s+/g, " ").trim();
13
+ }
14
+
15
+ function parseArgs(argv = []) {
16
+ const args = {};
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const item = argv[index];
19
+ if (item === "--domain") {
20
+ args.domain = normalizeText(argv[index + 1]).toLowerCase();
21
+ index += 1;
22
+ } else if (item === "--run-id") {
23
+ args.runId = normalizeText(argv[index + 1]);
24
+ index += 1;
25
+ }
26
+ }
27
+ return args;
28
+ }
29
+
30
+ function markFailed(domain, runId, error, options = {}) {
31
+ if (domain === "chat") {
32
+ return markBossChatDetachedWorkerFailed(runId, error, options);
33
+ }
34
+ if (domain === "recruit") {
35
+ return markBossRecruitDetachedWorkerFailed(runId, error, options);
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function installFailureHandlers(domain, runId) {
41
+ let handled = false;
42
+ const failOnce = (error, options = {}) => {
43
+ if (handled) return;
44
+ handled = true;
45
+ try {
46
+ markFailed(domain, runId, error, options);
47
+ } catch (markError) {
48
+ console.error("[boss-recommend-mcp] failed to persist detached worker failure", markError);
49
+ }
50
+ };
51
+
52
+ process.on("uncaughtException", (error) => {
53
+ console.error("[boss-recommend-mcp] detached worker uncaught exception", error);
54
+ failOnce(error, { code: "DETACHED_WORKER_UNCAUGHT_EXCEPTION" });
55
+ process.exit(1);
56
+ });
57
+
58
+ process.on("unhandledRejection", (reason) => {
59
+ console.error("[boss-recommend-mcp] detached worker unhandled rejection", reason);
60
+ const error = reason instanceof Error ? reason : new Error(normalizeText(reason) || "Unhandled promise rejection");
61
+ failOnce(error, { code: "DETACHED_WORKER_UNHANDLED_REJECTION" });
62
+ process.exit(1);
63
+ });
64
+
65
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
66
+ process.on(signal, () => {
67
+ const error = new Error(`detached ${domain} worker received ${signal}`);
68
+ console.error("[boss-recommend-mcp] detached worker received signal", signal);
69
+ failOnce(error, { code: "DETACHED_WORKER_SIGNAL" });
70
+ const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
71
+ process.exit(signalExitCodes[signal] || 1);
72
+ });
73
+ }
74
+ }
75
+
76
+ async function main() {
77
+ const options = parseArgs(process.argv.slice(2));
78
+ if (!options.domain || !options.runId) {
79
+ console.error("[boss-recommend-mcp] detached worker requires --domain and --run-id");
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ installFailureHandlers(options.domain, options.runId);
84
+ const result = options.domain === "chat"
85
+ ? await runBossChatDetachedWorker({ runId: options.runId })
86
+ : options.domain === "recruit"
87
+ ? await runBossRecruitDetachedWorker({ runId: options.runId })
88
+ : { ok: false, error: `Unsupported detached worker domain: ${options.domain}` };
89
+ if (!result?.ok) {
90
+ process.exitCode = 1;
91
+ }
92
+ }
93
+
94
+ await main().catch((error) => {
95
+ const options = parseArgs(process.argv.slice(2));
96
+ console.error("[boss-recommend-mcp] detached worker failed", error);
97
+ markFailed(options.domain, options.runId, error, { code: "DETACHED_WORKER_FAILED" });
98
+ process.exitCode = 1;
99
+ });