@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 +5 -2
- package/config/screening-config.example.json +1 -0
- package/package.json +2 -1
- package/skills/boss-chat/README.md +2 -1
- package/skills/boss-chat/SKILL.md +9 -1
- package/skills/boss-recommend-pipeline/README.md +1 -0
- package/skills/boss-recommend-pipeline/SKILL.md +5 -1
- package/skills/boss-recruit-pipeline/README.md +2 -0
- package/skills/boss-recruit-pipeline/SKILL.md +8 -0
- package/src/chat-mcp.js +397 -3
- package/src/cli.js +92 -50
- package/src/core/browser/index.js +162 -1
- package/src/core/self-heal/viewport.js +1 -1
- package/src/detached-worker.js +99 -0
- package/src/domains/chat/run-service.js +43 -35
- package/src/domains/recommend/run-service.js +6 -1
- package/src/domains/recruit/run-service.js +8 -1
- package/src/index.js +46 -3
- package/src/recruit-mcp.js +545 -11
package/src/recruit-mcp.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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(
|
|
1258
|
-
message: TERMINAL_STATUSES.has(
|
|
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(
|
|
1765
|
+
retryable: !TERMINAL_STATUSES.has(reconciledState)
|
|
1262
1766
|
},
|
|
1263
|
-
run:
|
|
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
|
}
|