@reconcrap/boss-recommend-mcp 2.0.56 → 2.0.57

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.0.56",
3
+ "version": "2.0.57",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -88,6 +88,7 @@
88
88
  "src/chat-mcp.js",
89
89
  "src/chat-runtime-config.js",
90
90
  "src/cli.js",
91
+ "src/detached-worker.js",
91
92
  "src/index.js",
92
93
  "src/parser.js",
93
94
  "src/recommend-mcp.js",
package/src/chat-mcp.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
4
6
  import {
5
7
  assertNoForbiddenCdpCalls,
6
8
  bringPageToFront,
@@ -18,7 +20,8 @@ import {
18
20
  RUN_STATUS_CANCELED,
19
21
  RUN_STATUS_COMPLETED,
20
22
  RUN_STATUS_FAILED,
21
- RUN_STATUS_PAUSED
23
+ RUN_STATUS_PAUSED,
24
+ RUN_STATUS_RUNNING
22
25
  } from "./core/run/index.js";
23
26
  import {
24
27
  buildLegacyScreenInputRows,
@@ -60,6 +63,8 @@ const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
60
63
  const CHAT_ALL_MAX_CANDIDATES = 100000;
61
64
  const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; numeric targets scan until that many candidates pass or the list ends; all/全部/扫到底 scans to the end";
62
65
  const RUN_MODE_ASYNC = "async";
66
+ const DETACHED_WORKER_SCRIPT = fileURLToPath(new URL("./detached-worker.js", import.meta.url));
67
+ const DETACHED_WORKER_POLL_MS = 1000;
63
68
 
64
69
  const CHAT_REQUIRED_FIELDS = Object.freeze([
65
70
  "job",
@@ -170,6 +175,9 @@ function getChatRunArtifacts(runId) {
170
175
  runs_dir: runsDir,
171
176
  output_dir: outputDir,
172
177
  run_state_path: path.join(runsDir, `${normalized}.json`),
178
+ detached_args_path: path.join(runsDir, `${normalized}.detached-args.json`),
179
+ worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
180
+ worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
173
181
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
174
182
  output_csv: path.join(outputDir, `${normalized}.results.csv`),
175
183
  report_json: path.join(outputDir, `${normalized}.report.json`)
@@ -256,6 +264,156 @@ function readChatRunState(runId) {
256
264
  return readJsonFile(artifacts.run_state_path);
257
265
  }
258
266
 
267
+ function writeChatRunState(runId, payload) {
268
+ const artifacts = getChatRunArtifacts(runId);
269
+ if (!artifacts) return null;
270
+ writeJsonAtomic(artifacts.run_state_path, payload);
271
+ return payload;
272
+ }
273
+
274
+ function createDetachedChatRunId() {
275
+ return `mcp_chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
276
+ }
277
+
278
+ function buildInitialChatDetachedState(runId, {
279
+ workspaceRoot = "",
280
+ args = {},
281
+ normalized = {},
282
+ pid = process.pid
283
+ } = {}) {
284
+ const artifacts = getChatRunArtifacts(runId);
285
+ const now = new Date().toISOString();
286
+ const isAllTarget = normalized.publicTargetCount === "all";
287
+ const processedLimit = isAllTarget ? CHAT_ALL_MAX_CANDIDATES : Math.max(1, Number(normalized.targetCount) || 1);
288
+ return {
289
+ run_id: runId,
290
+ mode: RUN_MODE_ASYNC,
291
+ state: "queued",
292
+ status: "queued",
293
+ stage: "queued",
294
+ started_at: now,
295
+ updated_at: now,
296
+ heartbeat_at: now,
297
+ completed_at: null,
298
+ pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
299
+ progress: {
300
+ target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
301
+ target_pass_count: isAllTarget ? null : normalized.targetCount ?? null,
302
+ processed_limit: processedLimit,
303
+ processed: 0,
304
+ screened: 0,
305
+ detail_opened: 0,
306
+ llm_screened: 0,
307
+ passed: 0,
308
+ skipped: 0,
309
+ requested: 0,
310
+ request_satisfied: 0,
311
+ request_skipped: 0,
312
+ greet_count: 0
313
+ },
314
+ last_message: "Boss chat detached worker is queued.",
315
+ context: {
316
+ domain: "chat",
317
+ target_url: CHAT_TARGET_URL,
318
+ workspace_root: normalizeText(workspaceRoot) || process.cwd(),
319
+ profile: normalized.profile || args.profile || "default",
320
+ job: normalized.job || args.job || "",
321
+ start_from: normalized.startFrom || args.start_from || "",
322
+ criteria: normalized.criteria || args.criteria || "",
323
+ greeting_text: normalized.greetingText || args.greeting_text || args.greetingText || DEFAULT_CHAT_GREETING_TEXT,
324
+ target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
325
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
326
+ request_resume_for_passed: shouldRequestChatResume(args),
327
+ detached_worker: true
328
+ },
329
+ control: {
330
+ pause_requested: false,
331
+ pause_requested_at: null,
332
+ pause_requested_by: null,
333
+ cancel_requested: false
334
+ },
335
+ resume: {
336
+ checkpoint_path: artifacts?.checkpoint_path || null,
337
+ pause_control_path: artifacts?.run_state_path || null,
338
+ output_csv: null,
339
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
340
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
341
+ resume_count: 0,
342
+ last_resumed_at: null,
343
+ last_paused_at: null
344
+ },
345
+ error: null,
346
+ result: null,
347
+ summary: null,
348
+ artifacts
349
+ };
350
+ }
351
+
352
+ function patchPersistedChatControl(runId, controlPatch = {}, {
353
+ status = "RUN_STATUS",
354
+ message = "",
355
+ lastMessage = ""
356
+ } = {}) {
357
+ const current = readChatRunState(runId);
358
+ if (!current) return null;
359
+ const state = normalizeText(current.state || current.status);
360
+ if (TERMINAL_STATUSES.has(state)) return null;
361
+ const now = new Date().toISOString();
362
+ const patched = {
363
+ ...current,
364
+ updated_at: now,
365
+ heartbeat_at: now,
366
+ last_message: lastMessage || message || current.last_message || "",
367
+ control: {
368
+ ...(current.control || {}),
369
+ ...controlPatch
370
+ }
371
+ };
372
+ writeChatRunState(runId, patched);
373
+ return {
374
+ status,
375
+ run: patched,
376
+ message,
377
+ persistence: {
378
+ source: "disk",
379
+ active_control_available: false,
380
+ detached_control_requested: true
381
+ },
382
+ runtime_evaluate_used: false,
383
+ method_summary: {},
384
+ method_log: [],
385
+ chrome: null
386
+ };
387
+ }
388
+
389
+ function launchDetachedChatWorker(runId) {
390
+ const artifacts = getChatRunArtifacts(runId);
391
+ if (!artifacts) throw new Error("Invalid chat run_id");
392
+ fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
393
+ const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
394
+ const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
395
+ let child;
396
+ try {
397
+ child = spawn(process.execPath, [
398
+ DETACHED_WORKER_SCRIPT,
399
+ "--domain",
400
+ "chat",
401
+ "--run-id",
402
+ runId
403
+ ], {
404
+ detached: true,
405
+ stdio: ["ignore", stdoutFd, stderrFd],
406
+ windowsHide: true,
407
+ env: process.env
408
+ });
409
+ } finally {
410
+ fs.closeSync(stdoutFd);
411
+ fs.closeSync(stderrFd);
412
+ }
413
+ if (typeof child?.unref === "function") child.unref();
414
+ return child;
415
+ }
416
+
259
417
  function toIsoOrNull(value) {
260
418
  const normalized = normalizeText(value);
261
419
  return normalized || null;
@@ -491,6 +649,27 @@ function finalizePersistedChatRun(persisted = {}, {
491
649
  return attachLegacyArtifactsToPersistedChatRun(next);
492
650
  }
493
651
 
652
+ export function markBossChatDetachedWorkerFailed(runId, error, options = {}) {
653
+ const normalizedRunId = normalizeRunId(runId);
654
+ if (!normalizedRunId) return null;
655
+ const persisted = readChatRunState(normalizedRunId) || buildInitialChatDetachedState(normalizedRunId, {});
656
+ const state = normalizeText(persisted.state || persisted.status);
657
+ if (TERMINAL_STATUSES.has(state)) return persisted;
658
+ const errorPayload = {
659
+ name: error?.name || "Error",
660
+ code: options.code || error?.code || "CHAT_WORKER_UNHANDLED_EXCEPTION",
661
+ message: normalizeText(error?.message || error || options.message) || "Boss chat detached worker exited unexpectedly."
662
+ };
663
+ if (normalizeText(error?.stack || "")) {
664
+ errorPayload.stack = String(error.stack).slice(0, 8000);
665
+ }
666
+ return finalizePersistedChatRun(persisted, {
667
+ status: RUN_STATUS_FAILED,
668
+ error: errorPayload,
669
+ message: errorPayload.message
670
+ });
671
+ }
672
+
494
673
  function persistedChatRunArtifactMissing(persisted = {}) {
495
674
  const runId = normalizeRunId(persisted.run_id || persisted.runId);
496
675
  const artifacts = getChatRunArtifacts(runId);
@@ -568,6 +747,8 @@ function buildLegacyChatResult(snapshot) {
568
747
  output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
569
748
  report_json: artifacts?.report_json || meta.reportJsonPath || null,
570
749
  checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
750
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
751
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
571
752
  started_at: snapshot.startedAt,
572
753
  completed_at: snapshot.completedAt || null,
573
754
  duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
@@ -624,6 +805,8 @@ function normalizeRunSnapshot(snapshot) {
624
805
  checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
625
806
  pause_control_path: artifacts?.run_state_path || null,
626
807
  output_csv: legacyResult?.output_csv || null,
808
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
809
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
627
810
  resume_count: meta.resumeCount || 0,
628
811
  last_resumed_at: meta.lastResumedAt || null,
629
812
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
@@ -1177,7 +1360,7 @@ function trackChatRun(runId) {
1177
1360
  });
1178
1361
  }
1179
1362
 
1180
- async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
1363
+ async function startBossChatRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
1181
1364
  const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
1182
1365
  const normalized = normalizeChatStartInput(args, defaultConfigResolution);
1183
1366
  const missingFields = getMissingChatStartFields(args, normalized);
@@ -1245,7 +1428,10 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
1245
1428
 
1246
1429
  let started;
1247
1430
  try {
1248
- started = chatRunService.startChatRun(getRunOptions(args, normalized, session, { workspaceRoot, configResolution }));
1431
+ started = chatRunService.startChatRun({
1432
+ ...getRunOptions(args, normalized, session, { workspaceRoot, configResolution }),
1433
+ runId
1434
+ });
1249
1435
  } catch (error) {
1250
1436
  await session.close?.();
1251
1437
  return {
@@ -1525,6 +1711,178 @@ export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {
1525
1711
  return attachMethodEvidence(started, started.run_id);
1526
1712
  }
1527
1713
 
1714
+ export async function startBossChatDetachedRunTool({ workspaceRoot = "", args = {} } = {}) {
1715
+ const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
1716
+ const normalized = normalizeChatStartInput(args, defaultConfigResolution);
1717
+ const missingFields = getMissingChatStartFields(args, normalized);
1718
+ if (missingFields.length) {
1719
+ return buildNeedInputResponse({
1720
+ args,
1721
+ missingFields,
1722
+ normalized
1723
+ });
1724
+ }
1725
+
1726
+ const useLlm = shouldUseChatLlm(args);
1727
+ const debugTestOptions = collectChatDebugTestOptions(args);
1728
+ if (debugTestOptions.length && !isDebugTestMode(args)) {
1729
+ return {
1730
+ status: "FAILED",
1731
+ error: {
1732
+ code: "DEBUG_TEST_MODE_REQUIRED",
1733
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1734
+ retryable: false
1735
+ },
1736
+ debug_test_options: debugTestOptions
1737
+ };
1738
+ }
1739
+ const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
1740
+ if (useLlm && !configResolution?.ok) {
1741
+ return {
1742
+ status: "FAILED",
1743
+ error: {
1744
+ code: "SCREEN_CONFIG_ERROR",
1745
+ message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
1746
+ retryable: true
1747
+ }
1748
+ };
1749
+ }
1750
+
1751
+ const runId = createDetachedChatRunId();
1752
+ const artifacts = getChatRunArtifacts(runId);
1753
+ const initial = buildInitialChatDetachedState(runId, {
1754
+ workspaceRoot,
1755
+ args,
1756
+ normalized,
1757
+ pid: process.pid
1758
+ });
1759
+ try {
1760
+ writeJsonAtomic(artifacts.detached_args_path, {
1761
+ domain: "chat",
1762
+ run_id: runId,
1763
+ workspace_root: normalizeText(workspaceRoot) || process.cwd(),
1764
+ args: clonePlain(args, {})
1765
+ });
1766
+ writeChatRunState(runId, initial);
1767
+ } catch (error) {
1768
+ return {
1769
+ status: "FAILED",
1770
+ error: {
1771
+ code: "CHAT_RUN_STATE_IO_ERROR",
1772
+ message: `Unable to write Boss chat detached run state: ${error?.message || error}`,
1773
+ retryable: false
1774
+ }
1775
+ };
1776
+ }
1777
+
1778
+ let child;
1779
+ try {
1780
+ child = launchDetachedChatWorker(runId);
1781
+ const now = new Date().toISOString();
1782
+ const latest = readChatRunState(runId) || initial;
1783
+ const latestState = normalizeText(latest.state || latest.status);
1784
+ if (TERMINAL_STATUSES.has(latestState)) {
1785
+ return {
1786
+ status: "FAILED",
1787
+ error: latest.error || {
1788
+ code: "CHAT_WORKER_LAUNCH_FAILED",
1789
+ message: "Boss chat detached worker exited during launch.",
1790
+ retryable: true
1791
+ },
1792
+ run: latest,
1793
+ runtime_evaluate_used: false,
1794
+ method_summary: {},
1795
+ method_log: [],
1796
+ chrome: null
1797
+ };
1798
+ }
1799
+ const queued = {
1800
+ ...latest,
1801
+ pid: child.pid || process.pid,
1802
+ updated_at: now,
1803
+ heartbeat_at: now,
1804
+ last_message: "Boss chat detached worker launched."
1805
+ };
1806
+ writeChatRunState(runId, queued);
1807
+ return {
1808
+ status: "ACCEPTED",
1809
+ run_id: runId,
1810
+ state: "queued",
1811
+ run: queued,
1812
+ poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1813
+ message: "Boss chat run started in a detached worker. It can continue after the MCP host returns or is recycled.",
1814
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
1815
+ detached_worker: true,
1816
+ runtime_evaluate_used: false,
1817
+ method_summary: {},
1818
+ method_log: [],
1819
+ chrome: null
1820
+ };
1821
+ } catch (error) {
1822
+ const failed = markBossChatDetachedWorkerFailed(runId, error, {
1823
+ code: "CHAT_WORKER_LAUNCH_FAILED",
1824
+ message: "Unable to launch Boss chat detached worker."
1825
+ });
1826
+ return {
1827
+ status: "FAILED",
1828
+ error: failed?.error || {
1829
+ code: "CHAT_WORKER_LAUNCH_FAILED",
1830
+ message: error?.message || "Unable to launch Boss chat detached worker.",
1831
+ retryable: true
1832
+ },
1833
+ run: failed || readChatRunState(runId),
1834
+ runtime_evaluate_used: false,
1835
+ method_summary: {},
1836
+ method_log: [],
1837
+ chrome: null
1838
+ };
1839
+ }
1840
+ }
1841
+
1842
+ export async function runBossChatDetachedWorker({ runId } = {}) {
1843
+ const normalizedRunId = normalizeRunId(runId);
1844
+ if (!normalizedRunId) return { ok: false, error: "run_id is required" };
1845
+ const artifacts = getChatRunArtifacts(normalizedRunId);
1846
+ const spec = readJsonFile(artifacts?.detached_args_path || "");
1847
+ if (!spec) {
1848
+ const error = new Error(`Boss chat detached args were not found for run_id=${normalizedRunId}`);
1849
+ markBossChatDetachedWorkerFailed(normalizedRunId, error, { code: "CHAT_WORKER_ARGS_MISSING" });
1850
+ return { ok: false, error: error.message };
1851
+ }
1852
+
1853
+ const started = await startBossChatRunInternal(spec.args || {}, {
1854
+ workspaceRoot: spec.workspace_root || "",
1855
+ runId: normalizedRunId
1856
+ });
1857
+ if (started?.status !== "ACCEPTED") {
1858
+ const failedError = started?.error || {
1859
+ code: "CHAT_WORKER_START_FAILED",
1860
+ message: started?.status || "Boss chat detached worker failed to start.",
1861
+ retryable: true
1862
+ };
1863
+ markBossChatDetachedWorkerFailed(normalizedRunId, failedError, {
1864
+ code: failedError.code || "CHAT_WORKER_START_FAILED"
1865
+ });
1866
+ return { ok: false, error: failedError.message || "Boss chat detached worker failed to start." };
1867
+ }
1868
+
1869
+ while (true) {
1870
+ const payload = getBossChatRunTool({ args: { run_id: normalizedRunId } });
1871
+ const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
1872
+ if (TERMINAL_STATUSES.has(state)) break;
1873
+ const persisted = readChatRunState(normalizedRunId);
1874
+ if (persisted?.control?.cancel_requested === true) {
1875
+ cancelBossChatRunTool({ args: { run_id: normalizedRunId } });
1876
+ } else if (persisted?.control?.pause_requested === true && state === RUN_STATUS_RUNNING) {
1877
+ pauseBossChatRunTool({ args: { run_id: normalizedRunId } });
1878
+ } else if (persisted?.control?.pause_requested === false && state === RUN_STATUS_PAUSED) {
1879
+ resumeBossChatRunTool({ args: { run_id: normalizedRunId } });
1880
+ }
1881
+ await sleep(DETACHED_WORKER_POLL_MS);
1882
+ }
1883
+ return { ok: true };
1884
+ }
1885
+
1528
1886
  export function getBossChatRunTool({ args = {} } = {}) {
1529
1887
  const runId = normalizeRunId(args.run_id || args.runId);
1530
1888
  if (!runId) {
@@ -1615,6 +1973,20 @@ export function pauseBossChatRunTool({ args = {} } = {}) {
1615
1973
  chrome: null
1616
1974
  };
1617
1975
  }
1976
+ if (persisted) {
1977
+ const reconciled = reconcilePersistedChatRun(persisted);
1978
+ if (reconciled.stale_finalized) return getBossChatRunTool({ args });
1979
+ return patchPersistedChatControl(runId, {
1980
+ pause_requested: true,
1981
+ pause_requested_at: new Date().toISOString(),
1982
+ pause_requested_by: "pause_boss_chat_run",
1983
+ cancel_requested: false
1984
+ }, {
1985
+ status: "PAUSE_REQUESTED",
1986
+ message: "暂停请求已写入 detached chat run 控制文件。",
1987
+ lastMessage: "暂停请求已写入 detached chat run 控制文件。"
1988
+ }) || getBossChatRunTool({ args });
1989
+ }
1618
1990
  return getBossChatRunTool({ args });
1619
1991
  }
1620
1992
  }
@@ -1665,6 +2037,18 @@ export function resumeBossChatRunTool({ args = {} } = {}) {
1665
2037
  if (persisted) {
1666
2038
  const reconciled = reconcilePersistedChatRun(persisted);
1667
2039
  const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
2040
+ if (!TERMINAL_STATUSES.has(reconciledStatus)) {
2041
+ return patchPersistedChatControl(runId, {
2042
+ pause_requested: false,
2043
+ pause_requested_at: null,
2044
+ pause_requested_by: null,
2045
+ cancel_requested: false
2046
+ }, {
2047
+ status: "RESUME_REQUESTED",
2048
+ message: "恢复请求已写入 detached chat run 控制文件。",
2049
+ lastMessage: "恢复请求已写入 detached chat run 控制文件。"
2050
+ }) || getBossChatRunTool({ args });
2051
+ }
1668
2052
  return {
1669
2053
  status: "FAILED",
1670
2054
  error: {
@@ -1743,6 +2127,16 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
1743
2127
  chrome: null
1744
2128
  };
1745
2129
  }
2130
+ return patchPersistedChatControl(runId, {
2131
+ pause_requested: true,
2132
+ pause_requested_at: new Date().toISOString(),
2133
+ pause_requested_by: "cancel_boss_chat_run",
2134
+ cancel_requested: true
2135
+ }, {
2136
+ status: "CANCEL_REQUESTED",
2137
+ message: "取消请求已写入 detached chat run 控制文件。",
2138
+ lastMessage: "取消请求已写入 detached chat run 控制文件。"
2139
+ }) || getBossChatRunTool({ args });
1746
2140
  }
1747
2141
  return getBossChatRunTool({ args });
1748
2142
  }
@@ -743,6 +743,7 @@ export function buildBossChromeLaunchArgs({
743
743
  ...LID_CLOSED_SAFE_CHROME_ARGS,
744
744
  ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
745
745
  ...extraArgs,
746
+ "--start-maximized",
746
747
  "--new-window",
747
748
  url
748
749
  ];
@@ -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
+ });
@@ -1876,9 +1876,10 @@ export function createChatRunService({
1876
1876
  } = {}) {
1877
1877
  const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1878
1878
 
1879
- function startChatRun({
1880
- client,
1881
- targetUrl = CHAT_TARGET_URL,
1879
+ function startChatRun({
1880
+ runId = "",
1881
+ client,
1882
+ targetUrl = CHAT_TARGET_URL,
1882
1883
  job = "",
1883
1884
  startFrom = "all",
1884
1885
  criteria = "",
@@ -1924,9 +1925,10 @@ export function createChatRunService({
1924
1925
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1925
1926
  });
1926
1927
  const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1927
- return manager.startRun({
1928
- name,
1929
- context: {
1928
+ return manager.startRun({
1929
+ runId,
1930
+ name,
1931
+ context: {
1930
1932
  domain: "chat",
1931
1933
  target_url: targetUrl,
1932
1934
  criteria_present: Boolean(criteria),
@@ -1146,6 +1146,7 @@ export function createRecruitRunService({
1146
1146
  const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1147
1147
 
1148
1148
  function startRecruitRun({
1149
+ runId = "",
1149
1150
  client,
1150
1151
  targetUrl = "",
1151
1152
  criteria = "",
@@ -1189,6 +1190,7 @@ export function createRecruitRunService({
1189
1190
  });
1190
1191
  const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1191
1192
  return manager.startRun({
1193
+ runId,
1192
1194
  name,
1193
1195
  context: {
1194
1196
  domain: "recruit",