@reconcrap/boss-recommend-mcp 2.0.55 → 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 +2 -1
- package/src/chat-mcp.js +397 -3
- package/src/core/browser/index.js +1 -0
- package/src/core/self-heal/viewport.js +1 -1
- package/src/detached-worker.js +99 -0
- package/src/domains/chat/run-service.js +8 -6
- package/src/domains/recommend/constants.js +17 -0
- package/src/domains/recommend/detail.js +355 -63
- package/src/domains/recommend/run-service.js +15 -0
- package/src/domains/recruit/run-service.js +2 -0
- package/src/index.js +35 -2
- package/src/recruit-mcp.js +534 -10
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
|
|
@@ -661,6 +947,8 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
661
947
|
checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
|
|
662
948
|
pause_control_path: artifacts?.run_state_path || null,
|
|
663
949
|
output_csv: legacyResult?.output_csv || null,
|
|
950
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
951
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
664
952
|
resume_count: meta.resumeCount || 0,
|
|
665
953
|
last_resumed_at: meta.lastResumedAt || null,
|
|
666
954
|
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
@@ -959,7 +1247,7 @@ function trackRecruitRun(runId) {
|
|
|
959
1247
|
});
|
|
960
1248
|
}
|
|
961
1249
|
|
|
962
|
-
async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
|
|
1250
|
+
async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
963
1251
|
const parsed = parseRecruitPipelineRequest(args);
|
|
964
1252
|
const gate = evaluateRecruitPipelineGate(parsed);
|
|
965
1253
|
if (gate) return gate;
|
|
@@ -1025,7 +1313,10 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
|
|
|
1025
1313
|
|
|
1026
1314
|
let started;
|
|
1027
1315
|
try {
|
|
1028
|
-
started = recruitRunService.startRecruitRun(
|
|
1316
|
+
started = recruitRunService.startRecruitRun({
|
|
1317
|
+
...getRunOptions(args, parsed, session, configResolution),
|
|
1318
|
+
runId
|
|
1319
|
+
});
|
|
1029
1320
|
} catch (error) {
|
|
1030
1321
|
await session.close?.();
|
|
1031
1322
|
return {
|
|
@@ -1117,6 +1408,179 @@ export async function startRecruitPipelineRunTool({ workspaceRoot = "", args = {
|
|
|
1117
1408
|
return attachMethodEvidence(started, started.run_id);
|
|
1118
1409
|
}
|
|
1119
1410
|
|
|
1411
|
+
export async function startRecruitPipelineDetachedRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1412
|
+
const normalizedArgs = {
|
|
1413
|
+
...args,
|
|
1414
|
+
execution_mode: RUN_MODE_ASYNC
|
|
1415
|
+
};
|
|
1416
|
+
const parsed = parseRecruitPipelineRequest(normalizedArgs);
|
|
1417
|
+
const gate = evaluateRecruitPipelineGate(parsed);
|
|
1418
|
+
if (gate) return gate;
|
|
1419
|
+
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1420
|
+
const screeningMode = normalizeScreeningModeArg(normalizedArgs);
|
|
1421
|
+
const debugTestOptions = collectRecruitDebugTestOptions(normalizedArgs);
|
|
1422
|
+
if (debugTestOptions.length && !isDebugTestMode(normalizedArgs)) {
|
|
1423
|
+
return {
|
|
1424
|
+
status: "FAILED",
|
|
1425
|
+
error: {
|
|
1426
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1427
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1428
|
+
retryable: false
|
|
1429
|
+
},
|
|
1430
|
+
debug_test_options: debugTestOptions
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
if (screeningMode === "llm" && !configResolution.ok) {
|
|
1434
|
+
return {
|
|
1435
|
+
status: "FAILED",
|
|
1436
|
+
error: {
|
|
1437
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1438
|
+
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
1439
|
+
retryable: true
|
|
1440
|
+
},
|
|
1441
|
+
config_path: configResolution.config_path || null,
|
|
1442
|
+
candidate_paths: configResolution.candidate_paths || []
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const runId = createDetachedRecruitRunId();
|
|
1447
|
+
const artifacts = getRecruitRunArtifacts(runId);
|
|
1448
|
+
const initial = buildInitialRecruitDetachedState(runId, {
|
|
1449
|
+
workspaceRoot,
|
|
1450
|
+
args: normalizedArgs,
|
|
1451
|
+
parsed,
|
|
1452
|
+
pid: globalThis.process?.pid
|
|
1453
|
+
});
|
|
1454
|
+
try {
|
|
1455
|
+
writeJsonAtomic(artifacts.detached_args_path, {
|
|
1456
|
+
domain: "recruit",
|
|
1457
|
+
run_id: runId,
|
|
1458
|
+
workspace_root: normalizeText(workspaceRoot) || globalThis.process?.cwd?.() || "",
|
|
1459
|
+
args: clonePlain(normalizedArgs, {})
|
|
1460
|
+
});
|
|
1461
|
+
writeRecruitRunState(runId, initial);
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
return {
|
|
1464
|
+
status: "FAILED",
|
|
1465
|
+
error: {
|
|
1466
|
+
code: "RECRUIT_RUN_STATE_IO_ERROR",
|
|
1467
|
+
message: `Unable to write Boss search detached run state: ${error?.message || error}`,
|
|
1468
|
+
retryable: false
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
try {
|
|
1474
|
+
const child = launchDetachedRecruitWorker(runId);
|
|
1475
|
+
const now = new Date().toISOString();
|
|
1476
|
+
const latest = readRecruitRunState(runId) || initial;
|
|
1477
|
+
const latestState = normalizeText(latest.state || latest.status);
|
|
1478
|
+
if (TERMINAL_STATUSES.has(latestState)) {
|
|
1479
|
+
return {
|
|
1480
|
+
status: "FAILED",
|
|
1481
|
+
error: latest.error || {
|
|
1482
|
+
code: "RECRUIT_WORKER_LAUNCH_FAILED",
|
|
1483
|
+
message: "Boss search detached worker exited during launch.",
|
|
1484
|
+
retryable: true
|
|
1485
|
+
},
|
|
1486
|
+
run: latest,
|
|
1487
|
+
runtime_evaluate_used: false,
|
|
1488
|
+
method_summary: {},
|
|
1489
|
+
method_log: [],
|
|
1490
|
+
chrome: null
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const queued = {
|
|
1494
|
+
...latest,
|
|
1495
|
+
pid: child.pid || globalThis.process?.pid || null,
|
|
1496
|
+
updated_at: now,
|
|
1497
|
+
heartbeat_at: now,
|
|
1498
|
+
last_message: "Boss search detached worker launched."
|
|
1499
|
+
};
|
|
1500
|
+
writeRecruitRunState(runId, queued);
|
|
1501
|
+
return {
|
|
1502
|
+
status: "ACCEPTED",
|
|
1503
|
+
run_id: runId,
|
|
1504
|
+
state: "queued",
|
|
1505
|
+
run: queued,
|
|
1506
|
+
poll_after_sec: DEFAULT_RECRUIT_POLL_AFTER_SEC,
|
|
1507
|
+
review: parsed.review,
|
|
1508
|
+
message: "Boss search run started in a detached worker. It can continue after the MCP host returns or is recycled.",
|
|
1509
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
1510
|
+
detached_worker: true,
|
|
1511
|
+
runtime_evaluate_used: false,
|
|
1512
|
+
method_summary: {},
|
|
1513
|
+
method_log: [],
|
|
1514
|
+
chrome: null
|
|
1515
|
+
};
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
const failed = markBossRecruitDetachedWorkerFailed(runId, error, {
|
|
1518
|
+
code: "RECRUIT_WORKER_LAUNCH_FAILED",
|
|
1519
|
+
message: "Unable to launch Boss search detached worker."
|
|
1520
|
+
});
|
|
1521
|
+
return {
|
|
1522
|
+
status: "FAILED",
|
|
1523
|
+
error: failed?.error || {
|
|
1524
|
+
code: "RECRUIT_WORKER_LAUNCH_FAILED",
|
|
1525
|
+
message: error?.message || "Unable to launch Boss search detached worker.",
|
|
1526
|
+
retryable: true
|
|
1527
|
+
},
|
|
1528
|
+
run: failed || readRecruitRunState(runId),
|
|
1529
|
+
runtime_evaluate_used: false,
|
|
1530
|
+
method_summary: {},
|
|
1531
|
+
method_log: [],
|
|
1532
|
+
chrome: null
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
export async function runBossRecruitDetachedWorker({ runId } = {}) {
|
|
1538
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
1539
|
+
if (!normalizedRunId) return { ok: false, error: "run_id is required" };
|
|
1540
|
+
const artifacts = getRecruitRunArtifacts(normalizedRunId);
|
|
1541
|
+
const spec = readJsonFile(artifacts?.detached_args_path || "");
|
|
1542
|
+
if (!spec) {
|
|
1543
|
+
const error = new Error(`Boss search detached args were not found for run_id=${normalizedRunId}`);
|
|
1544
|
+
markBossRecruitDetachedWorkerFailed(normalizedRunId, error, { code: "RECRUIT_WORKER_ARGS_MISSING" });
|
|
1545
|
+
return { ok: false, error: error.message };
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const started = await startRecruitPipelineRunInternal({
|
|
1549
|
+
...(spec.args || {}),
|
|
1550
|
+
execution_mode: RUN_MODE_ASYNC
|
|
1551
|
+
}, {
|
|
1552
|
+
workspaceRoot: spec.workspace_root || "",
|
|
1553
|
+
runId: normalizedRunId
|
|
1554
|
+
});
|
|
1555
|
+
if (started?.status !== "ACCEPTED") {
|
|
1556
|
+
const failedError = started?.error || {
|
|
1557
|
+
code: "RECRUIT_WORKER_START_FAILED",
|
|
1558
|
+
message: started?.status || "Boss search detached worker failed to start.",
|
|
1559
|
+
retryable: true
|
|
1560
|
+
};
|
|
1561
|
+
markBossRecruitDetachedWorkerFailed(normalizedRunId, failedError, {
|
|
1562
|
+
code: failedError.code || "RECRUIT_WORKER_START_FAILED"
|
|
1563
|
+
});
|
|
1564
|
+
return { ok: false, error: failedError.message || "Boss search detached worker failed to start." };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
while (true) {
|
|
1568
|
+
const payload = getRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1569
|
+
const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
|
|
1570
|
+
if (TERMINAL_STATUSES.has(state)) break;
|
|
1571
|
+
const persisted = readRecruitRunState(normalizedRunId);
|
|
1572
|
+
if (persisted?.control?.cancel_requested === true) {
|
|
1573
|
+
cancelRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1574
|
+
} else if (persisted?.control?.pause_requested === true && state === RUN_STATUS_RUNNING) {
|
|
1575
|
+
pauseRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1576
|
+
} else if (persisted?.control?.pause_requested === false && state === RUN_STATUS_PAUSED) {
|
|
1577
|
+
resumeRecruitPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1578
|
+
}
|
|
1579
|
+
await sleep(DETACHED_WORKER_POLL_MS);
|
|
1580
|
+
}
|
|
1581
|
+
return { ok: true };
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1120
1584
|
export function getRecruitPipelineRunTool({ args = {} } = {}) {
|
|
1121
1585
|
const runId = normalizeText(args.run_id);
|
|
1122
1586
|
if (!runId) {
|
|
@@ -1139,12 +1603,14 @@ export function getRecruitPipelineRunTool({ args = {} } = {}) {
|
|
|
1139
1603
|
} catch {
|
|
1140
1604
|
const persisted = readRecruitRunState(runId);
|
|
1141
1605
|
if (persisted) {
|
|
1606
|
+
const reconciled = reconcilePersistedRecruitRun(persisted);
|
|
1142
1607
|
return {
|
|
1143
1608
|
status: "RUN_STATUS",
|
|
1144
|
-
run:
|
|
1609
|
+
run: reconciled.run,
|
|
1145
1610
|
persistence: {
|
|
1146
1611
|
source: "disk",
|
|
1147
|
-
active_control_available: false
|
|
1612
|
+
active_control_available: false,
|
|
1613
|
+
stale_finalized: reconciled.stale_finalized === true
|
|
1148
1614
|
},
|
|
1149
1615
|
runtime_evaluate_used: false,
|
|
1150
1616
|
method_summary: {},
|
|
@@ -1203,6 +1669,20 @@ export function pauseRecruitPipelineRunTool({ args = {} } = {}) {
|
|
|
1203
1669
|
chrome: null
|
|
1204
1670
|
};
|
|
1205
1671
|
}
|
|
1672
|
+
if (persisted) {
|
|
1673
|
+
const reconciled = reconcilePersistedRecruitRun(persisted);
|
|
1674
|
+
if (reconciled.stale_finalized) return getRecruitPipelineRunTool({ args });
|
|
1675
|
+
return patchPersistedRecruitControl(runId, {
|
|
1676
|
+
pause_requested: true,
|
|
1677
|
+
pause_requested_at: new Date().toISOString(),
|
|
1678
|
+
pause_requested_by: "pause_recruit_pipeline_run",
|
|
1679
|
+
cancel_requested: false
|
|
1680
|
+
}, {
|
|
1681
|
+
status: "PAUSE_REQUESTED",
|
|
1682
|
+
message: "暂停请求已写入 detached search run 控制文件。",
|
|
1683
|
+
lastMessage: "暂停请求已写入 detached search run 控制文件。"
|
|
1684
|
+
}) || getRecruitPipelineRunTool({ args });
|
|
1685
|
+
}
|
|
1206
1686
|
return getRecruitPipelineRunTool({ args });
|
|
1207
1687
|
}
|
|
1208
1688
|
}
|
|
@@ -1251,19 +1731,34 @@ export function resumeRecruitPipelineRunTool({ args = {} } = {}) {
|
|
|
1251
1731
|
} catch {
|
|
1252
1732
|
const persisted = readRecruitRunState(runId);
|
|
1253
1733
|
if (persisted) {
|
|
1734
|
+
const reconciled = reconcilePersistedRecruitRun(persisted);
|
|
1735
|
+
const reconciledState = reconciled.run?.state || reconciled.run?.status;
|
|
1736
|
+
if (!TERMINAL_STATUSES.has(reconciledState)) {
|
|
1737
|
+
return patchPersistedRecruitControl(runId, {
|
|
1738
|
+
pause_requested: false,
|
|
1739
|
+
pause_requested_at: null,
|
|
1740
|
+
pause_requested_by: null,
|
|
1741
|
+
cancel_requested: false
|
|
1742
|
+
}, {
|
|
1743
|
+
status: "RESUME_REQUESTED",
|
|
1744
|
+
message: "恢复请求已写入 detached search run 控制文件。",
|
|
1745
|
+
lastMessage: "恢复请求已写入 detached search run 控制文件。"
|
|
1746
|
+
}) || getRecruitPipelineRunTool({ args });
|
|
1747
|
+
}
|
|
1254
1748
|
return {
|
|
1255
1749
|
status: TERMINAL_STATUSES.has(persisted.state) ? "FAILED" : "FAILED",
|
|
1256
1750
|
error: {
|
|
1257
|
-
code: TERMINAL_STATUSES.has(
|
|
1258
|
-
message: TERMINAL_STATUSES.has(
|
|
1751
|
+
code: TERMINAL_STATUSES.has(reconciledState) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
|
|
1752
|
+
message: TERMINAL_STATUSES.has(reconciledState)
|
|
1259
1753
|
? "目标任务已结束,无法继续。"
|
|
1260
1754
|
: "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
|
|
1261
|
-
retryable: !TERMINAL_STATUSES.has(
|
|
1755
|
+
retryable: !TERMINAL_STATUSES.has(reconciledState)
|
|
1262
1756
|
},
|
|
1263
|
-
run:
|
|
1757
|
+
run: reconciled.run,
|
|
1264
1758
|
persistence: {
|
|
1265
1759
|
source: "disk",
|
|
1266
|
-
active_control_available: false
|
|
1760
|
+
active_control_available: false,
|
|
1761
|
+
stale_finalized: reconciled.stale_finalized === true
|
|
1267
1762
|
},
|
|
1268
1763
|
runtime_evaluate_used: false,
|
|
1269
1764
|
method_summary: {},
|
|
@@ -1307,6 +1802,35 @@ export function cancelRecruitPipelineRunTool({ args = {} } = {}) {
|
|
|
1307
1802
|
chrome: null
|
|
1308
1803
|
};
|
|
1309
1804
|
}
|
|
1805
|
+
if (persisted) {
|
|
1806
|
+
const reconciled = reconcilePersistedRecruitRun(persisted, { cancelStale: true });
|
|
1807
|
+
if (reconciled.stale_finalized) {
|
|
1808
|
+
return {
|
|
1809
|
+
status: "CANCEL_REQUESTED",
|
|
1810
|
+
run: reconciled.run,
|
|
1811
|
+
message: "该 search run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
|
|
1812
|
+
persistence: {
|
|
1813
|
+
source: "disk",
|
|
1814
|
+
active_control_available: false,
|
|
1815
|
+
stale_finalized: true
|
|
1816
|
+
},
|
|
1817
|
+
runtime_evaluate_used: false,
|
|
1818
|
+
method_summary: {},
|
|
1819
|
+
method_log: [],
|
|
1820
|
+
chrome: null
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
return patchPersistedRecruitControl(runId, {
|
|
1824
|
+
pause_requested: true,
|
|
1825
|
+
pause_requested_at: new Date().toISOString(),
|
|
1826
|
+
pause_requested_by: "cancel_recruit_pipeline_run",
|
|
1827
|
+
cancel_requested: true
|
|
1828
|
+
}, {
|
|
1829
|
+
status: "CANCEL_REQUESTED",
|
|
1830
|
+
message: "取消请求已写入 detached search run 控制文件。",
|
|
1831
|
+
lastMessage: "取消请求已写入 detached search run 控制文件。"
|
|
1832
|
+
}) || getRecruitPipelineRunTool({ args });
|
|
1833
|
+
}
|
|
1310
1834
|
return getRecruitPipelineRunTool({ args });
|
|
1311
1835
|
}
|
|
1312
1836
|
}
|