@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.
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
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,
@@ -46,12 +49,19 @@ const DEFAULT_RECRUIT_HOST = "127.0.0.1";
46
49
  const DEFAULT_RECRUIT_PORT = 9222;
47
50
  const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
48
51
  const DEFAULT_RECRUIT_HOME_DIR = ".boss-recruit-mcp";
52
+ const DETACHED_WORKER_SCRIPT = fileURLToPath(new URL("./detached-worker.js", import.meta.url));
53
+ const DETACHED_WORKER_POLL_MS = 1000;
49
54
 
50
55
  const TERMINAL_STATUSES = new Set([
51
56
  RUN_STATUS_COMPLETED,
52
57
  RUN_STATUS_FAILED,
53
58
  RUN_STATUS_CANCELED
54
59
  ]);
60
+ const STALE_PROCESS_STATUSES = new Set([
61
+ "queued",
62
+ "running",
63
+ RUN_STATUS_CANCELING
64
+ ]);
55
65
 
56
66
  let recruitWorkflowImpl = runRecruitWorkflow;
57
67
  let recruitConnectorImpl = connectRecruitChromeSession;
@@ -141,6 +151,9 @@ function getRecruitRunArtifacts(runId) {
141
151
  runs_dir: runsDir,
142
152
  output_dir: outputDir,
143
153
  run_state_path: path.join(runsDir, `${normalized}.json`),
154
+ detached_args_path: path.join(runsDir, `${normalized}.detached-args.json`),
155
+ worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
156
+ worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
144
157
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
145
158
  output_csv: path.join(outputDir, `${normalized}.results.csv`),
146
159
  report_json: path.join(outputDir, `${normalized}.report.json`)
@@ -215,6 +228,162 @@ function readRecruitRunState(runId) {
215
228
  return readJsonFile(artifacts.run_state_path);
216
229
  }
217
230
 
231
+ function writeRecruitRunState(runId, payload) {
232
+ const artifacts = getRecruitRunArtifacts(runId);
233
+ if (!artifacts) return null;
234
+ writeJsonAtomic(artifacts.run_state_path, payload);
235
+ return payload;
236
+ }
237
+
238
+ function createDetachedRecruitRunId() {
239
+ return `mcp_recruit_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
240
+ }
241
+
242
+ function isPidAlive(pid) {
243
+ const numericPid = Number(pid);
244
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
245
+ if (numericPid === globalThis.process?.pid) return true;
246
+ try {
247
+ globalThis.process.kill(numericPid, 0);
248
+ return true;
249
+ } catch (error) {
250
+ return error?.code === "EPERM";
251
+ }
252
+ }
253
+
254
+ function buildInitialRecruitDetachedState(runId, {
255
+ workspaceRoot = "",
256
+ args = {},
257
+ parsed = {},
258
+ pid = globalThis.process?.pid
259
+ } = {}) {
260
+ const artifacts = getRecruitRunArtifacts(runId);
261
+ const now = new Date().toISOString();
262
+ const targetCount = parsePositiveInteger(args.max_candidates, parsed.screenParams?.target_count || 10);
263
+ return {
264
+ run_id: runId,
265
+ mode: RUN_MODE_ASYNC,
266
+ state: "queued",
267
+ status: "queued",
268
+ stage: "queued",
269
+ started_at: now,
270
+ updated_at: now,
271
+ heartbeat_at: now,
272
+ completed_at: null,
273
+ pid: Number.isInteger(pid) && pid > 0 ? pid : globalThis.process?.pid || null,
274
+ progress: {
275
+ target_count: targetCount,
276
+ processed: 0,
277
+ screened: 0,
278
+ detail_opened: 0,
279
+ llm_screened: 0,
280
+ passed: 0,
281
+ skipped: 0,
282
+ greet_count: 0
283
+ },
284
+ last_message: "Boss search detached worker is queued.",
285
+ context: {
286
+ domain: "recruit",
287
+ target_url: RECRUIT_TARGET_URL,
288
+ workspace_root: normalizeText(workspaceRoot) || globalThis.process?.cwd?.() || "",
289
+ instruction: args.instruction || "",
290
+ confirmation: clonePlain(args.confirmation || {}, {}),
291
+ overrides: clonePlain(args.overrides || {}, {}),
292
+ search_params: clonePlain(parsed.searchParams || {}, {}),
293
+ criteria_present: Boolean(parsed.screenParams?.criteria),
294
+ max_candidates: targetCount,
295
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
296
+ detached_worker: true,
297
+ rounds: []
298
+ },
299
+ control: {
300
+ pause_requested: false,
301
+ pause_requested_at: null,
302
+ pause_requested_by: null,
303
+ cancel_requested: false
304
+ },
305
+ resume: {
306
+ checkpoint_path: artifacts?.checkpoint_path || null,
307
+ pause_control_path: artifacts?.run_state_path || null,
308
+ output_csv: null,
309
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
310
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
311
+ resume_count: 0,
312
+ last_resumed_at: null,
313
+ last_paused_at: null
314
+ },
315
+ error: null,
316
+ result: null,
317
+ summary: null,
318
+ artifacts
319
+ };
320
+ }
321
+
322
+ function patchPersistedRecruitControl(runId, controlPatch = {}, {
323
+ status = "RUN_STATUS",
324
+ message = "",
325
+ lastMessage = ""
326
+ } = {}) {
327
+ const current = readRecruitRunState(runId);
328
+ if (!current) return null;
329
+ const state = normalizeText(current.state || current.status);
330
+ if (TERMINAL_STATUSES.has(state)) return null;
331
+ const now = new Date().toISOString();
332
+ const patched = {
333
+ ...current,
334
+ updated_at: now,
335
+ heartbeat_at: now,
336
+ last_message: lastMessage || message || current.last_message || "",
337
+ control: {
338
+ ...(current.control || {}),
339
+ ...controlPatch
340
+ }
341
+ };
342
+ writeRecruitRunState(runId, patched);
343
+ return {
344
+ status,
345
+ run: patched,
346
+ message,
347
+ persistence: {
348
+ source: "disk",
349
+ active_control_available: false,
350
+ detached_control_requested: true
351
+ },
352
+ runtime_evaluate_used: false,
353
+ method_summary: {},
354
+ method_log: [],
355
+ chrome: null
356
+ };
357
+ }
358
+
359
+ function launchDetachedRecruitWorker(runId) {
360
+ const artifacts = getRecruitRunArtifacts(runId);
361
+ if (!artifacts) throw new Error("Invalid recruit run_id");
362
+ fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
363
+ const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
364
+ const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
365
+ let child;
366
+ try {
367
+ child = spawn(globalThis.process.execPath, [
368
+ DETACHED_WORKER_SCRIPT,
369
+ "--domain",
370
+ "recruit",
371
+ "--run-id",
372
+ runId
373
+ ], {
374
+ detached: true,
375
+ stdio: ["ignore", stdoutFd, stderrFd],
376
+ windowsHide: true,
377
+ env: globalThis.process.env
378
+ });
379
+ } finally {
380
+ fs.closeSync(stdoutFd);
381
+ fs.closeSync(stderrFd);
382
+ }
383
+ if (typeof child?.unref === "function") child.unref();
384
+ return child;
385
+ }
386
+
218
387
  function ensureRecruitRunArtifacts(snapshot) {
219
388
  const artifacts = getRecruitRunArtifacts(snapshot?.runId || snapshot?.run_id);
220
389
  if (!artifacts) return null;
@@ -307,6 +476,121 @@ function completionReason(status) {
307
476
  return null;
308
477
  }
309
478
 
479
+ function snapshotFromPersistedRecruitRun(persisted = {}) {
480
+ return {
481
+ runId: persisted.run_id || persisted.runId,
482
+ name: persisted.name || persisted.run_id || persisted.runId,
483
+ status: persisted.status || persisted.state,
484
+ phase: persisted.stage || persisted.phase,
485
+ progress: persisted.progress || {},
486
+ context: persisted.context || {},
487
+ checkpoint: persisted.checkpoint || {},
488
+ startedAt: persisted.started_at || persisted.startedAt,
489
+ updatedAt: persisted.updated_at || persisted.updatedAt,
490
+ completedAt: persisted.completed_at || persisted.completedAt || null,
491
+ error: persisted.error || null,
492
+ summary: persisted.summary || null
493
+ };
494
+ }
495
+
496
+ function attachLegacyArtifactsToPersistedRecruitRun(persisted = {}) {
497
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
498
+ if (!runId) return persisted;
499
+ const snapshot = snapshotFromPersistedRecruitRun(persisted);
500
+ const result = buildLegacyRunResult(snapshot);
501
+ const artifacts = getRecruitRunArtifacts(runId);
502
+ const next = {
503
+ ...persisted,
504
+ result,
505
+ resume: {
506
+ ...(persisted.resume || {}),
507
+ checkpoint_path: result?.checkpoint_path || persisted.resume?.checkpoint_path || artifacts?.checkpoint_path || null,
508
+ output_csv: result?.output_csv || persisted.resume?.output_csv || artifacts?.output_csv || null,
509
+ worker_stdout_path: artifacts?.worker_stdout_path || persisted.resume?.worker_stdout_path || null,
510
+ worker_stderr_path: artifacts?.worker_stderr_path || persisted.resume?.worker_stderr_path || null
511
+ },
512
+ artifacts: artifacts || persisted.artifacts || null
513
+ };
514
+ return writeRecruitRunState(runId, next);
515
+ }
516
+
517
+ function finalizePersistedRecruitRun(persisted = {}, {
518
+ status = RUN_STATUS_FAILED,
519
+ error = null,
520
+ message = ""
521
+ } = {}) {
522
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
523
+ if (!runId) return persisted;
524
+ const now = new Date().toISOString();
525
+ const normalizedError = status === RUN_STATUS_FAILED
526
+ ? {
527
+ name: error?.name || "Error",
528
+ code: error?.code || "STALE_RUN_PROCESS_EXITED",
529
+ message: error?.message || message || "Boss search run process exited before it wrote a terminal state."
530
+ }
531
+ : null;
532
+ const next = {
533
+ ...persisted,
534
+ run_id: runId,
535
+ state: status,
536
+ status,
537
+ stage: persisted.stage || persisted.phase || "recruit:stale",
538
+ updated_at: now,
539
+ heartbeat_at: now,
540
+ completed_at: persisted.completed_at || now,
541
+ last_message: normalizedError?.message || message || status,
542
+ control: {
543
+ ...(persisted.control || {}),
544
+ cancel_requested: false
545
+ },
546
+ error: normalizedError,
547
+ summary: persisted.summary || null
548
+ };
549
+ return attachLegacyArtifactsToPersistedRecruitRun(next);
550
+ }
551
+
552
+ function reconcilePersistedRecruitRun(persisted = {}, { cancelStale = false } = {}) {
553
+ const status = persisted.status || persisted.state;
554
+ if (STALE_PROCESS_STATUSES.has(status) && !isPidAlive(persisted.pid)) {
555
+ const shouldCancel = cancelStale || status === RUN_STATUS_CANCELING || persisted.control?.cancel_requested === true;
556
+ return {
557
+ run: finalizePersistedRecruitRun(persisted, {
558
+ status: shouldCancel ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
559
+ error: shouldCancel ? null : {
560
+ code: "STALE_RUN_PROCESS_EXITED",
561
+ message: `Boss search run process is no longer alive for pid=${persisted.pid || "unknown"}.`
562
+ },
563
+ message: shouldCancel
564
+ ? "Boss search run was canceled after its worker process was no longer active."
565
+ : `Boss search run process is no longer alive for pid=${persisted.pid || "unknown"}.`
566
+ }),
567
+ stale_finalized: true
568
+ };
569
+ }
570
+ return { run: persisted };
571
+ }
572
+
573
+ export function markBossRecruitDetachedWorkerFailed(runId, error, options = {}) {
574
+ const normalizedRunId = normalizeRunId(runId);
575
+ if (!normalizedRunId) return null;
576
+ const persisted = readRecruitRunState(normalizedRunId) || buildInitialRecruitDetachedState(normalizedRunId, {});
577
+ const state = normalizeText(persisted.state || persisted.status);
578
+ if (TERMINAL_STATUSES.has(state)) return persisted;
579
+ const errorPayload = {
580
+ name: error?.name || "Error",
581
+ code: options.code || error?.code || "RECRUIT_WORKER_UNHANDLED_EXCEPTION",
582
+ message: normalizeText(error?.message || error || options.message) || "Boss search detached worker exited unexpectedly."
583
+ };
584
+ if (normalizeText(error?.stack || "")) {
585
+ errorPayload.stack = String(error.stack).slice(0, 8000);
586
+ }
587
+ return finalizePersistedRecruitRun(persisted, {
588
+ status: RUN_STATUS_FAILED,
589
+ error: errorPayload,
590
+ message: errorPayload.message
591
+ });
592
+ }
593
+
310
594
  function buildLegacyRunResult(snapshot) {
311
595
  if (!snapshot) return null;
312
596
  const artifacts = ensureRecruitRunArtifacts(snapshot);
@@ -341,6 +625,8 @@ function buildLegacyRunResult(snapshot) {
341
625
  duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt || snapshot.updatedAt),
342
626
  output_csv: summary?.output_csv || meta.outputCsvPath || artifacts?.output_csv || null,
343
627
  report_json: summary?.report_json || meta.reportJsonPath || artifacts?.report_json || null,
628
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
629
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
344
630
  round_count: 1,
345
631
  current_round_index: 1,
346
632
  checkpoint_path: snapshot.checkpoint?.checkpoint_path
@@ -379,7 +665,17 @@ function createHumanBehaviorInputSchema(description = "可选,search/recruit
379
665
  listScrollJitter: { type: "boolean" },
380
666
  shortRest: { type: "boolean" },
381
667
  batchRest: { type: "boolean" },
382
- actionCooldown: { type: "boolean" }
668
+ actionCooldown: { type: "boolean" },
669
+ restLevel: {
670
+ type: "string",
671
+ enum: ["low", "medium", "high"],
672
+ description: "本次 run 的休息强度:low 保持旧策略;medium 约 5 小时/700 人累计休息 30 分钟;high 约 5 小时/700 人累计休息 1 小时"
673
+ },
674
+ rest_level: {
675
+ type: "string",
676
+ enum: ["low", "medium", "high"],
677
+ description: "兼容字段;优先使用 restLevel"
678
+ }
383
679
  },
384
680
  additionalProperties: false,
385
681
  description
@@ -661,6 +957,8 @@ function normalizeRunSnapshot(snapshot) {
661
957
  checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
662
958
  pause_control_path: artifacts?.run_state_path || null,
663
959
  output_csv: legacyResult?.output_csv || null,
960
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
961
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
664
962
  resume_count: meta.resumeCount || 0,
665
963
  last_resumed_at: meta.lastResumedAt || null,
666
964
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
@@ -959,7 +1257,7 @@ function trackRecruitRun(runId) {
959
1257
  });
960
1258
  }
961
1259
 
962
- async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
1260
+ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
963
1261
  const parsed = parseRecruitPipelineRequest(args);
964
1262
  const gate = evaluateRecruitPipelineGate(parsed);
965
1263
  if (gate) return gate;
@@ -1025,7 +1323,10 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
1025
1323
 
1026
1324
  let started;
1027
1325
  try {
1028
- started = recruitRunService.startRecruitRun(getRunOptions(args, parsed, session, configResolution));
1326
+ started = recruitRunService.startRecruitRun({
1327
+ ...getRunOptions(args, parsed, session, configResolution),
1328
+ runId
1329
+ });
1029
1330
  } catch (error) {
1030
1331
  await session.close?.();
1031
1332
  return {
@@ -1117,6 +1418,179 @@ export async function startRecruitPipelineRunTool({ workspaceRoot = "", args = {
1117
1418
  return attachMethodEvidence(started, started.run_id);
1118
1419
  }
1119
1420
 
1421
+ export async function startRecruitPipelineDetachedRunTool({ workspaceRoot = "", args = {} } = {}) {
1422
+ const normalizedArgs = {
1423
+ ...args,
1424
+ execution_mode: RUN_MODE_ASYNC
1425
+ };
1426
+ const parsed = parseRecruitPipelineRequest(normalizedArgs);
1427
+ const gate = evaluateRecruitPipelineGate(parsed);
1428
+ if (gate) return gate;
1429
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1430
+ const screeningMode = normalizeScreeningModeArg(normalizedArgs);
1431
+ const debugTestOptions = collectRecruitDebugTestOptions(normalizedArgs);
1432
+ if (debugTestOptions.length && !isDebugTestMode(normalizedArgs)) {
1433
+ return {
1434
+ status: "FAILED",
1435
+ error: {
1436
+ code: "DEBUG_TEST_MODE_REQUIRED",
1437
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1438
+ retryable: false
1439
+ },
1440
+ debug_test_options: debugTestOptions
1441
+ };
1442
+ }
1443
+ if (screeningMode === "llm" && !configResolution.ok) {
1444
+ return {
1445
+ status: "FAILED",
1446
+ error: {
1447
+ code: "SCREEN_CONFIG_ERROR",
1448
+ message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
1449
+ retryable: true
1450
+ },
1451
+ config_path: configResolution.config_path || null,
1452
+ candidate_paths: configResolution.candidate_paths || []
1453
+ };
1454
+ }
1455
+
1456
+ const runId = createDetachedRecruitRunId();
1457
+ const artifacts = getRecruitRunArtifacts(runId);
1458
+ const initial = buildInitialRecruitDetachedState(runId, {
1459
+ workspaceRoot,
1460
+ args: normalizedArgs,
1461
+ parsed,
1462
+ pid: globalThis.process?.pid
1463
+ });
1464
+ try {
1465
+ writeJsonAtomic(artifacts.detached_args_path, {
1466
+ domain: "recruit",
1467
+ run_id: runId,
1468
+ workspace_root: normalizeText(workspaceRoot) || globalThis.process?.cwd?.() || "",
1469
+ args: clonePlain(normalizedArgs, {})
1470
+ });
1471
+ writeRecruitRunState(runId, initial);
1472
+ } catch (error) {
1473
+ return {
1474
+ status: "FAILED",
1475
+ error: {
1476
+ code: "RECRUIT_RUN_STATE_IO_ERROR",
1477
+ message: `Unable to write Boss search detached run state: ${error?.message || error}`,
1478
+ retryable: false
1479
+ }
1480
+ };
1481
+ }
1482
+
1483
+ try {
1484
+ const child = launchDetachedRecruitWorker(runId);
1485
+ const now = new Date().toISOString();
1486
+ const latest = readRecruitRunState(runId) || initial;
1487
+ const latestState = normalizeText(latest.state || latest.status);
1488
+ if (TERMINAL_STATUSES.has(latestState)) {
1489
+ return {
1490
+ status: "FAILED",
1491
+ error: latest.error || {
1492
+ code: "RECRUIT_WORKER_LAUNCH_FAILED",
1493
+ message: "Boss search detached worker exited during launch.",
1494
+ retryable: true
1495
+ },
1496
+ run: latest,
1497
+ runtime_evaluate_used: false,
1498
+ method_summary: {},
1499
+ method_log: [],
1500
+ chrome: null
1501
+ };
1502
+ }
1503
+ const queued = {
1504
+ ...latest,
1505
+ pid: child.pid || globalThis.process?.pid || null,
1506
+ updated_at: now,
1507
+ heartbeat_at: now,
1508
+ last_message: "Boss search detached worker launched."
1509
+ };
1510
+ writeRecruitRunState(runId, queued);
1511
+ return {
1512
+ status: "ACCEPTED",
1513
+ run_id: runId,
1514
+ state: "queued",
1515
+ run: queued,
1516
+ poll_after_sec: DEFAULT_RECRUIT_POLL_AFTER_SEC,
1517
+ review: parsed.review,
1518
+ message: "Boss search run started in a detached worker. It can continue after the MCP host returns or is recycled.",
1519
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
1520
+ detached_worker: true,
1521
+ runtime_evaluate_used: false,
1522
+ method_summary: {},
1523
+ method_log: [],
1524
+ chrome: null
1525
+ };
1526
+ } catch (error) {
1527
+ const failed = markBossRecruitDetachedWorkerFailed(runId, error, {
1528
+ code: "RECRUIT_WORKER_LAUNCH_FAILED",
1529
+ message: "Unable to launch Boss search detached worker."
1530
+ });
1531
+ return {
1532
+ status: "FAILED",
1533
+ error: failed?.error || {
1534
+ code: "RECRUIT_WORKER_LAUNCH_FAILED",
1535
+ message: error?.message || "Unable to launch Boss search detached worker.",
1536
+ retryable: true
1537
+ },
1538
+ run: failed || readRecruitRunState(runId),
1539
+ runtime_evaluate_used: false,
1540
+ method_summary: {},
1541
+ method_log: [],
1542
+ chrome: null
1543
+ };
1544
+ }
1545
+ }
1546
+
1547
+ export async function runBossRecruitDetachedWorker({ runId } = {}) {
1548
+ const normalizedRunId = normalizeRunId(runId);
1549
+ if (!normalizedRunId) return { ok: false, error: "run_id is required" };
1550
+ const artifacts = getRecruitRunArtifacts(normalizedRunId);
1551
+ const spec = readJsonFile(artifacts?.detached_args_path || "");
1552
+ if (!spec) {
1553
+ const error = new Error(`Boss search detached args were not found for run_id=${normalizedRunId}`);
1554
+ markBossRecruitDetachedWorkerFailed(normalizedRunId, error, { code: "RECRUIT_WORKER_ARGS_MISSING" });
1555
+ return { ok: false, error: error.message };
1556
+ }
1557
+
1558
+ const started = await startRecruitPipelineRunInternal({
1559
+ ...(spec.args || {}),
1560
+ execution_mode: RUN_MODE_ASYNC
1561
+ }, {
1562
+ workspaceRoot: spec.workspace_root || "",
1563
+ runId: normalizedRunId
1564
+ });
1565
+ if (started?.status !== "ACCEPTED") {
1566
+ const failedError = started?.error || {
1567
+ code: "RECRUIT_WORKER_START_FAILED",
1568
+ message: started?.status || "Boss search detached worker failed to start.",
1569
+ retryable: true
1570
+ };
1571
+ markBossRecruitDetachedWorkerFailed(normalizedRunId, failedError, {
1572
+ code: failedError.code || "RECRUIT_WORKER_START_FAILED"
1573
+ });
1574
+ return { ok: false, error: failedError.message || "Boss search detached worker failed to start." };
1575
+ }
1576
+
1577
+ while (true) {
1578
+ const payload = getRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
1579
+ const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
1580
+ if (TERMINAL_STATUSES.has(state)) break;
1581
+ const persisted = readRecruitRunState(normalizedRunId);
1582
+ if (persisted?.control?.cancel_requested === true) {
1583
+ cancelRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
1584
+ } else if (persisted?.control?.pause_requested === true && state === RUN_STATUS_RUNNING) {
1585
+ pauseRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
1586
+ } else if (persisted?.control?.pause_requested === false && state === RUN_STATUS_PAUSED) {
1587
+ resumeRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
1588
+ }
1589
+ await sleep(DETACHED_WORKER_POLL_MS);
1590
+ }
1591
+ return { ok: true };
1592
+ }
1593
+
1120
1594
  export function getRecruitPipelineRunTool({ args = {} } = {}) {
1121
1595
  const runId = normalizeText(args.run_id);
1122
1596
  if (!runId) {
@@ -1139,12 +1613,14 @@ export function getRecruitPipelineRunTool({ args = {} } = {}) {
1139
1613
  } catch {
1140
1614
  const persisted = readRecruitRunState(runId);
1141
1615
  if (persisted) {
1616
+ const reconciled = reconcilePersistedRecruitRun(persisted);
1142
1617
  return {
1143
1618
  status: "RUN_STATUS",
1144
- run: persisted,
1619
+ run: reconciled.run,
1145
1620
  persistence: {
1146
1621
  source: "disk",
1147
- active_control_available: false
1622
+ active_control_available: false,
1623
+ stale_finalized: reconciled.stale_finalized === true
1148
1624
  },
1149
1625
  runtime_evaluate_used: false,
1150
1626
  method_summary: {},
@@ -1203,6 +1679,20 @@ export function pauseRecruitPipelineRunTool({ args = {} } = {}) {
1203
1679
  chrome: null
1204
1680
  };
1205
1681
  }
1682
+ if (persisted) {
1683
+ const reconciled = reconcilePersistedRecruitRun(persisted);
1684
+ if (reconciled.stale_finalized) return getRecruitPipelineRunTool({ args });
1685
+ return patchPersistedRecruitControl(runId, {
1686
+ pause_requested: true,
1687
+ pause_requested_at: new Date().toISOString(),
1688
+ pause_requested_by: "pause_recruit_pipeline_run",
1689
+ cancel_requested: false
1690
+ }, {
1691
+ status: "PAUSE_REQUESTED",
1692
+ message: "暂停请求已写入 detached search run 控制文件。",
1693
+ lastMessage: "暂停请求已写入 detached search run 控制文件。"
1694
+ }) || getRecruitPipelineRunTool({ args });
1695
+ }
1206
1696
  return getRecruitPipelineRunTool({ args });
1207
1697
  }
1208
1698
  }
@@ -1251,19 +1741,34 @@ export function resumeRecruitPipelineRunTool({ args = {} } = {}) {
1251
1741
  } catch {
1252
1742
  const persisted = readRecruitRunState(runId);
1253
1743
  if (persisted) {
1744
+ const reconciled = reconcilePersistedRecruitRun(persisted);
1745
+ const reconciledState = reconciled.run?.state || reconciled.run?.status;
1746
+ if (!TERMINAL_STATUSES.has(reconciledState)) {
1747
+ return patchPersistedRecruitControl(runId, {
1748
+ pause_requested: false,
1749
+ pause_requested_at: null,
1750
+ pause_requested_by: null,
1751
+ cancel_requested: false
1752
+ }, {
1753
+ status: "RESUME_REQUESTED",
1754
+ message: "恢复请求已写入 detached search run 控制文件。",
1755
+ lastMessage: "恢复请求已写入 detached search run 控制文件。"
1756
+ }) || getRecruitPipelineRunTool({ args });
1757
+ }
1254
1758
  return {
1255
1759
  status: TERMINAL_STATUSES.has(persisted.state) ? "FAILED" : "FAILED",
1256
1760
  error: {
1257
- code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1258
- message: TERMINAL_STATUSES.has(persisted.state)
1761
+ code: TERMINAL_STATUSES.has(reconciledState) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1762
+ message: TERMINAL_STATUSES.has(reconciledState)
1259
1763
  ? "目标任务已结束,无法继续。"
1260
1764
  : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1261
- retryable: !TERMINAL_STATUSES.has(persisted.state)
1765
+ retryable: !TERMINAL_STATUSES.has(reconciledState)
1262
1766
  },
1263
- run: persisted,
1767
+ run: reconciled.run,
1264
1768
  persistence: {
1265
1769
  source: "disk",
1266
- active_control_available: false
1770
+ active_control_available: false,
1771
+ stale_finalized: reconciled.stale_finalized === true
1267
1772
  },
1268
1773
  runtime_evaluate_used: false,
1269
1774
  method_summary: {},
@@ -1307,6 +1812,35 @@ export function cancelRecruitPipelineRunTool({ args = {} } = {}) {
1307
1812
  chrome: null
1308
1813
  };
1309
1814
  }
1815
+ if (persisted) {
1816
+ const reconciled = reconcilePersistedRecruitRun(persisted, { cancelStale: true });
1817
+ if (reconciled.stale_finalized) {
1818
+ return {
1819
+ status: "CANCEL_REQUESTED",
1820
+ run: reconciled.run,
1821
+ message: "该 search run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
1822
+ persistence: {
1823
+ source: "disk",
1824
+ active_control_available: false,
1825
+ stale_finalized: true
1826
+ },
1827
+ runtime_evaluate_used: false,
1828
+ method_summary: {},
1829
+ method_log: [],
1830
+ chrome: null
1831
+ };
1832
+ }
1833
+ return patchPersistedRecruitControl(runId, {
1834
+ pause_requested: true,
1835
+ pause_requested_at: new Date().toISOString(),
1836
+ pause_requested_by: "cancel_recruit_pipeline_run",
1837
+ cancel_requested: true
1838
+ }, {
1839
+ status: "CANCEL_REQUESTED",
1840
+ message: "取消请求已写入 detached search run 控制文件。",
1841
+ lastMessage: "取消请求已写入 detached search run 控制文件。"
1842
+ }) || getRecruitPipelineRunTool({ args });
1843
+ }
1310
1844
  return getRecruitPipelineRunTool({ args });
1311
1845
  }
1312
1846
  }