@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/README.md CHANGED
@@ -259,13 +259,16 @@ config/screening-config.example.json
259
259
  - `debugPort`:未显式传 `port` 时,recommend / search / chat CDP-only MCP run 和健康检查默认连接这个 Chrome 调试端口。
260
260
  - `outputDir`:recommend / search / chat 完成后的最终 CSV 与 report JSON 会写入这里;run state / checkpoint 仍保留在各自状态目录,方便 pause/resume/cancel。
261
261
  - `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。
262
- - `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests" }`。用于 recommend / search / chat 的可靠性实验,支持:
262
+ - `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests", "restLevel": "low" }`。用于 recommend / search / chat 的可靠性实验,支持:
263
263
  - `profile: "baseline"`:关闭人类节奏,保持确定性行为。
264
264
  - `profile: "paced"`:启用 CDP-only Bezier 鼠标移动、较大按钮的安全 inset 点击点、分块 `Input.insertText`、列表 wheel/settle jitter,以及小的动作前后读秒。
265
265
  - `profile: "paced_with_rests"`:在 `paced` 基础上启用候选人短休和批次休息。
266
+ - `restLevel: "low"`:保持旧版休息策略不变,候选人短休 8% 概率暂停 3-7 秒,批次休息约每 25-32 人暂停 15-30 秒。
267
+ - `restLevel: "medium"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 30 分钟。
268
+ - `restLevel: "high"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 1 小时。
266
269
  - `humanRestEnabled`:兼容旧配置。设为 `true` 时等价于 `humanBehavior.profile="paced_with_rests"`;设为 `false` 时不会关闭当前默认节奏。如需关闭,请显式设置 `humanBehavior.enabled=false` 或 `humanBehavior.profile="baseline"`。
267
270
  - recommend / search / chat 图片简历 fallback 与主列表滚动都会在启用 `listScrollJitter` 时使用 coverage-safe scroll jitter:每次 wheel delta 在安全范围内变化,并保留截图重叠、重复检测、bottom-marker / stop-boundary 逻辑,实际 delta 和 settle 时间会写入 artifact metadata。
268
- - chat/recommend/search run 也兼容显式参数 `safe_pacing` 与 `batch_rest_enabled`:run 参数优先于配置文件。
271
+ - chat/recommend/search run 也兼容显式参数 `safe_pacing`、`batch_rest_enabled` 与 `human_behavior.restLevel`:run 参数优先于配置文件。AI harness/skill 启动每次 run 前必须让用户明确选择 `low/medium/high`,再把选择写入 `human_behavior.restLevel`。
269
272
 
270
273
  ## 常用命令
271
274
 
@@ -16,6 +16,7 @@
16
16
  "humanBehavior": {
17
17
  "enabled": true,
18
18
  "profile": "paced_with_rests",
19
+ "restLevel": "low",
19
20
  "clickMovement": true,
20
21
  "textEntry": true,
21
22
  "listScrollJitter": true,
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.1.0",
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",
@@ -17,7 +17,7 @@ Please run a Boss chat-only task (do not switch to recommend flow).
17
17
  Execution order:
18
18
  1) Call boss_chat_health_check.
19
19
  2) Call prepare_boss_chat_run once (empty params allowed) to fetch job_options and missing fields.
20
- 3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria.
20
+ 3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria, rest_level (low/medium/high).
21
21
  4) After user reply, call start_boss_chat_run exactly once to start the run.
22
22
  5) If ACCEPTED, reply only with run_id and "task started"; no auto polling.
23
23
 
@@ -27,6 +27,7 @@ Anti-loop rules:
27
27
  - Do not use start_boss_chat_run for preflight. It is only for the final start call and must include job/start_from/target_count/criteria.
28
28
  - Do not call start_boss_chat_run repeatedly in one turn.
29
29
  - Do not call get_boss_chat_run unless user explicitly asks for progress.
30
+ - Do not choose rest_level for the user. Pass the user's choice as `human_behavior.restLevel`.
30
31
 
31
32
  target_count mapping:
32
33
  - Positive integer means explicit cap (for example 20).
@@ -27,6 +27,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
27
27
  - `start_from`: `unread|all`
28
28
  - `target_count`
29
29
  - `criteria`
30
+ - `rest_level`: `low|medium|high`
30
31
 
31
32
  可选:
32
33
 
@@ -38,6 +39,12 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
38
39
  - `safe_pacing`
39
40
  - `batch_rest_enabled`
40
41
 
42
+ 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
43
+
44
+ ```json
45
+ { "human_behavior": { "restLevel": "medium" } }
46
+ ```
47
+
41
48
  `greeting_text` 默认规则:
42
49
 
43
50
  - 本次显式传入 `greeting_text`:使用本次值
@@ -62,6 +69,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
62
69
  - 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
63
70
  - `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
64
71
  - `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
72
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
65
73
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
66
74
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
67
75
  - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
@@ -89,5 +97,5 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
89
97
 
90
98
  - 用结构化中文。
91
99
  - 首轮建议先调用一次 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
92
- - 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`。
100
+ - 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`、`rest_level`。
93
101
  - 若健康检查失败,明确提示共享配置文件 `screening-config.json` 不可用。
@@ -7,6 +7,7 @@
7
7
  - 先确认推荐页 filters
8
8
  - 再确认筛选 criteria
9
9
  - 再确认本次运行统一动作 `favorite` 或 `greet`
10
+ - 每次运行都要让用户明确选择休息强度 `low` / `medium` / `high`,并传入 `human_behavior.restLevel`
10
11
  - 只确认一次 `post_action`,不要逐个候选人反复确认
11
12
  - 运行中临时中断请使用 `pause_recommend_pipeline_run`(按 run_id),不要靠自然语言“暂停/继续”指令
12
13
  - 继续执行请使用 `resume_recommend_pipeline_run`;状态查询默认按用户指令触发,不自动轮询
@@ -20,7 +20,8 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
20
20
  - **确认不可代填(强制)**
21
21
  - 禁止 agent 自行“设置合理参数”并代替用户确认。
22
22
  - 禁止在用户未明确回复前,把任意 `*_confirmed` 字段设为 `true`。
23
- - 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job`。
23
+ - 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job/rest_level`。
24
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
24
25
  - 若工具返回 `pending_questions`,必须逐项向用户提问并等待用户回复;不得跳过提问直接执行。
25
26
 
26
27
  - **岗位确认时机**
@@ -54,6 +55,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
54
55
  - `target_count`(可空)
55
56
  - `post_action`:`favorite|greet|none`
56
57
  - `max_greet_count`(仅当 `post_action=greet`)
58
+ - `rest_level`:`low|medium|high`
57
59
 
58
60
  ### Stage B (页面就绪后)
59
61
 
@@ -82,6 +84,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
82
84
  - `gender`: `不限/男/女`
83
85
  - `recent_not_view`: `不限/近14天没有`
84
86
  - `post_action`: `favorite/greet/none`
87
+ - `rest_level`: `low/medium/high`
85
88
 
86
89
  ## Tool Usage
87
90
 
@@ -94,6 +97,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
94
97
  - 关键输入:
95
98
  - `confirmation`:`page_confirmed/page_value/filters_confirmed/school_tag_confirmed.../job_confirmed/job_value/final_confirmed`
96
99
  - `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
100
+ - `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
97
101
  - 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
98
102
 
99
103
  最小策略:
@@ -15,3 +15,5 @@ This skill intentionally replaces legacy `boss-recruit-mcp` skill installs. It r
15
15
  - `cancel_recruit_pipeline_run`
16
16
 
17
17
  Do not call the old `@reconcrap/boss-recruit-mcp` package from this skill.
18
+
19
+ Each run must ask the user to choose `rest_level` (`low` / `medium` / `high`) and pass the answer as `human_behavior.restLevel`; do not pick a default for the user.
@@ -32,6 +32,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
32
32
  - 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件、post action、max greet 等参数,必须逐项传入或确认。
33
33
  - `post_action=greet` 时必须确认 `max_greet_count`;不要默认等于 `target_count`。
34
34
  - 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
35
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
35
36
 
36
37
  ## Required Inputs
37
38
 
@@ -39,6 +40,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
39
40
  - `keyword` 或用户明确的搜索意图
40
41
  - `criteria`
41
42
  - `target_count`
43
+ - `rest_level`: `low|medium|high`
42
44
 
43
45
  常用可选项:
44
46
 
@@ -50,6 +52,12 @@ description: "Use when users want Boss search/recruit-page screening via the uni
50
52
  - `max_greet_count`
51
53
  - `port`
52
54
 
55
+ 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
56
+
57
+ ```json
58
+ { "human_behavior": { "restLevel": "medium" } }
59
+ ```
60
+
53
61
  ## Response Style
54
62
 
55
63
  - 用结构化中文确认参数。
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
  }